├── .gitignore ├── .editorconfig ├── .maven-bintray.xml ├── src ├── test │ ├── resources │ │ ├── logback-test.xml │ │ ├── schema_hsqldb.sql │ │ ├── schema_derby.sql │ │ ├── schema_h2.sql │ │ ├── schema_mysql.sql │ │ ├── schema_postgresql.sql │ │ ├── schema_mssql.sql │ │ └── schema_oracle.sql │ ├── groovy │ │ └── cz │ │ │ └── jirutka │ │ │ └── spring │ │ │ └── data │ │ │ └── jdbc │ │ │ ├── TestUtils.groovy │ │ │ ├── internal │ │ │ ├── IterableUtilsTest.groovy │ │ │ ├── StringUtilsTest.groovy │ │ │ └── ObjectUtilsTest.groovy │ │ │ ├── sql │ │ │ ├── LimitOffsetSqlGeneratorTest.groovy │ │ │ ├── SqlGeneratorFactoryIT.groovy │ │ │ ├── SQL2008SqlGeneratorTest.groovy │ │ │ ├── Oracle9SqlGeneratorTest.groovy │ │ │ ├── SqlGeneratorFactoryTest.groovy │ │ │ └── SqlGeneratorTest.groovy │ │ │ ├── fixtures │ │ │ ├── CommentWithUser.groovy │ │ │ ├── Comment.groovy │ │ │ ├── User.groovy │ │ │ └── BoardingPass.groovy │ │ │ ├── BaseJdbcRepositoryTest.groovy │ │ │ ├── StandaloneUsageIT.groovy │ │ │ ├── MssqlJdbcRepositoryIT.groovy │ │ │ ├── H2JdbcRepositoryIT.groovy │ │ │ ├── HsqldbJdbcRepositoryIT.groovy │ │ │ ├── MySqlJdbcRepositoryIT.groovy │ │ │ ├── config │ │ │ └── AbstractTestConfig.groovy │ │ │ ├── OracleJdbcRepositoryIT.groovy │ │ │ ├── MariaDbJdbcRepositoryIT.groovy │ │ │ ├── JdbcRepositoryGeneratedKeyIT.groovy │ │ │ ├── PostgresqlJdbcRepositoryIT.groovy │ │ │ ├── DerbyJdbcRepositoryIT.groovy │ │ │ ├── Mssql2012JdbcRepositoryIT.groovy │ │ │ ├── JdbcRepositoryCompoundPkIT.groovy │ │ │ ├── JdbcRepositoryManyToOneIT.groovy │ │ │ └── JdbcRepositoryManualKeyIT.groovy │ └── java │ │ └── cz │ │ └── jirutka │ │ └── spring │ │ └── data │ │ └── jdbc │ │ └── fixtures │ │ ├── BoardingPassRepository.java │ │ ├── UserRepository.java │ │ ├── CommentRepository.java │ │ └── CommentWithUserRepository.java └── main │ └── java │ └── cz │ └── jirutka │ └── spring │ └── data │ └── jdbc │ ├── RowUnmapper.java │ ├── UnsupportedRowUnmapper.java │ ├── internal │ ├── IterableUtils.java │ ├── ObjectUtils.java │ └── StringUtils.java │ ├── sql │ ├── LimitOffsetSqlGenerator.java │ ├── Oracle9SqlGenerator.java │ ├── SQL2008SqlGenerator.java │ ├── SqlGenerator.java │ ├── SqlGeneratorFactory.java │ └── DefaultSqlGenerator.java │ ├── NoRecordUpdatedException.java │ ├── JdbcRepository.java │ ├── TableDescription.java │ └── BaseJdbcRepository.java ├── script ├── travis-setup ├── travis-deploy ├── appveyor-install-maven.ps1 ├── travis-install-oracle └── appveyor-setup-mssql.ps1 ├── .appveyor.yml ├── .travis.yml ├── CHANGELOG.adoc ├── pom.xml ├── LICENSE └── README.adoc /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | *.iml 4 | *.log 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{adoc,sql,yml}] 13 | indent_size = 2 14 | 15 | [script/*] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.maven-bintray.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | jfrog-oss-snapshot-local 9 | jirutka 10 | ${env.BINTRAY_API_KEY} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | %d{HH:mm:ss:SSS} | %-5level | %thread | %logger{20} | %msg%n%rEx 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/test/resources/schema_hsqldb.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE USERS ( 2 | user_name varchar(256) PRIMARY KEY, 3 | date_of_birth TIMESTAMP NOT NULL, 4 | reputation INT NOT NULL, 5 | enabled BOOLEAN NOT NULL 6 | ); 7 | 8 | CREATE TABLE COMMENTS ( 9 | id INT IDENTITY PRIMARY KEY, 10 | user_name varchar(256), 11 | contents varchar(1000), 12 | created_time TIMESTAMP NOT NULL, 13 | favourite_count INT NOT NULL, 14 | FOREIGN KEY (user_name) REFERENCES USERS(user_name) 15 | ); 16 | 17 | CREATE TABLE BOARDING_PASS ( 18 | flight_no varchar(8) NOT NULL, 19 | seq_no INT NOT NULL, 20 | passenger VARCHAR(1000), 21 | seat CHAR(3), 22 | PRIMARY KEY (flight_no, seq_no) 23 | ); 24 | -------------------------------------------------------------------------------- /src/test/resources/schema_derby.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE USERS ( 2 | user_name varchar(256) NOT NULL PRIMARY KEY, 3 | date_of_birth TIMESTAMP NOT NULL, 4 | reputation INT NOT NULL, 5 | enabled BOOLEAN NOT NULL 6 | ); 7 | 8 | CREATE TABLE COMMENTS ( 9 | id INT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, 10 | user_name varchar(256), 11 | contents varchar(1000), 12 | created_time TIMESTAMP NOT NULL, 13 | favourite_count INT NOT NULL, 14 | FOREIGN KEY (user_name) REFERENCES USERS(user_name) 15 | ); 16 | 17 | CREATE TABLE BOARDING_PASS ( 18 | flight_no varchar(8) NOT NULL, 19 | seq_no INT NOT NULL, 20 | passenger VARCHAR(1000), 21 | seat CHAR(3), 22 | PRIMARY KEY (flight_no, seq_no) 23 | ); 24 | -------------------------------------------------------------------------------- /src/test/resources/schema_h2.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS USERS ( 2 | user_name varchar(256) PRIMARY KEY, 3 | date_of_birth DATE NOT NULL, 4 | reputation INT NOT NULL, 5 | enabled BOOLEAN NOT NULL 6 | ); 7 | 8 | CREATE TABLE IF NOT EXISTS COMMENTS ( 9 | id INT AUTO_INCREMENT PRIMARY KEY, 10 | user_name varchar(256), 11 | contents varchar(1000), 12 | created_time TIMESTAMP NOT NULL, 13 | favourite_count INT NOT NULL, 14 | FOREIGN KEY (user_name) REFERENCES USERS(user_name) 15 | ); 16 | 17 | CREATE TABLE IF NOT EXISTS BOARDING_PASS ( 18 | flight_no varchar(8) NOT NULL, 19 | seq_no INT NOT NULL, 20 | passenger VARCHAR(1000), 21 | seat CHAR(3), 22 | PRIMARY KEY (flight_no, seq_no) 23 | ); 24 | -------------------------------------------------------------------------------- /src/test/resources/schema_mysql.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS USERS; 2 | DROP TABLE IF EXISTS COMMENTS; 3 | DROP TABLE IF EXISTS BOARDING_PASS; 4 | 5 | CREATE TABLE USERS ( 6 | user_name varchar(255), 7 | date_of_birth TIMESTAMP NOT NULL, 8 | reputation INT NOT NULL, 9 | enabled BIT(1) NOT NULL, 10 | PRIMARY KEY (user_name) 11 | ); 12 | 13 | CREATE TABLE COMMENTS ( 14 | id INT AUTO_INCREMENT, 15 | user_name varchar(256), 16 | contents varchar(1000), 17 | created_time TIMESTAMP NOT NULL, 18 | favourite_count INT NOT NULL, 19 | PRIMARY KEY (id) 20 | ); 21 | 22 | CREATE TABLE BOARDING_PASS ( 23 | flight_no VARCHAR(8) NOT NULL, 24 | seq_no INT NOT NULL, 25 | passenger VARCHAR(1000), 26 | seat CHAR(3), 27 | PRIMARY KEY (flight_no, seq_no) 28 | ); 29 | -------------------------------------------------------------------------------- /script/travis-setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ts=4: 3 | set -o errexit 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | case "$DB" in 8 | postgresql) 9 | psql -U postgres -c 'create database spring_data_jdbc_repository_test;' 10 | ;; 11 | mariadb | mysql) 12 | mysql -e 'create database spring_data_jdbc_repository_test;' 13 | ;; 14 | oracle) 15 | # Workaround for nasty buffer overflow 16 | # https://github.com/travis-ci/travis-ci/issues/5227 17 | sudo hostname "$(hostname | cut -c1-63)" 18 | sed -e "s/^\(127\.0\.0\.1.*\)/\1 $(hostname | cut -c 1-63)/" /etc/hosts \ 19 | | sudo tee /etc/hosts 20 | 21 | script/travis-install-oracle 22 | 23 | # import schema 24 | echo exit | "$ORACLE_HOME"/bin/sqlplus test/test @src/test/resources/schema_oracle.sql 25 | ;; 26 | esac 27 | -------------------------------------------------------------------------------- /script/travis-deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ts=4: 3 | set -o errexit 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if [[ "$TRAVIS_PULL_REQUEST" != 'false' ]]; then 8 | echo 'This is a pull request, skipping deploy.'; exit 0 9 | fi 10 | 11 | if [[ -z "$BINTRAY_API_KEY" ]]; then 12 | echo '$BINTRAY_API_KEY is not set, skipping deploy.'; exit 0 13 | fi 14 | 15 | if [[ "$TRAVIS_BRANCH" != 'master' ]]; then 16 | echo 'This is not the master branch, skipping deploy.'; exit 0 17 | fi 18 | 19 | if [[ "$TRAVIS_JDK_VERSION" != 'oraclejdk8' || "$DB" != 'embedded' ]]; then 20 | echo 'This is not the build job we are looking for, skipping deploy.'; exit 0 21 | fi 22 | 23 | echo '==> Deploying artifact to JFrog OSS Maven repository' 24 | mvn deploy --settings .maven-bintray.xml -Dgpg.skip=true -DskipTests=true 25 | -------------------------------------------------------------------------------- /src/test/resources/schema_postgresql.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users CASCADE; 2 | DROP TABLE IF EXISTS comments CASCADE; 3 | DROP TABLE IF EXISTS boarding_pass CASCADE; 4 | 5 | CREATE TABLE users ( 6 | user_name text PRIMARY KEY, 7 | date_of_birth date NOT NULL, 8 | reputation integer NOT NULL, 9 | enabled boolean NOT NULL 10 | ); 11 | 12 | CREATE TABLE comments ( 13 | id serial PRIMARY KEY, 14 | user_name text REFERENCES users, 15 | contents text, 16 | created_time timestamp NOT NULL, 17 | favourite_count integer NOT NULL 18 | ); 19 | 20 | CREATE TABLE boarding_pass ( 21 | flight_no varchar(8) NOT NULL, 22 | seq_no integer NOT NULL, 23 | passenger text, 24 | seat char(3), 25 | PRIMARY KEY (flight_no, seq_no) 26 | ); 27 | -------------------------------------------------------------------------------- /script/appveyor-install-maven.ps1: -------------------------------------------------------------------------------- 1 | # Source: http://www.yegor256.com/2015/01/10/windows-appveyor-maven.html 2 | 3 | $version = $args[0] 4 | $url = "http://www.apache.org/dist/maven/maven-3/${version}/binaries/apache-maven-${version}-bin.zip" 5 | $destDir = "C:\maven" 6 | $mavenHome = "${destDir}\apache-maven-${version}" 7 | 8 | echo "Installing Maven $version into $destDir" 9 | 10 | Add-Type -AssemblyName System.IO.Compression.FileSystem 11 | if (!(Test-Path -Path "$destDir" )) { 12 | (new-object System.Net.WebClient).DownloadFile("$url", "C:\maven-bin.zip") 13 | [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\maven-bin.zip", "$destDir") 14 | } 15 | 16 | # Set environment variables. 17 | $env:PATH = "${mavenHome}\bin;${env:JAVA_HOME}\bin;${env:PATH}" 18 | $env:MAVEN_OPTS = "-XX:MaxPermSize=1g -Xmx1g" 19 | -------------------------------------------------------------------------------- /script/travis-install-oracle: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ts=4: 3 | set -o errexit -o pipefail 4 | 5 | TRAVIS_ORACLE_URL='https://github.com/cbandy/travis-oracle/archive/v2.0.0.tar.gz' 6 | 7 | export ORACLE_COOKIE='sqldev' 8 | export ORACLE_FILE='oracle11g/xe/oracle-xe-11.2.0-1.0.x86_64.rpm.zip' 9 | 10 | echo '==> Installing Oracle XE' 11 | 12 | mkdir -p .travis/oracle 13 | wget -O - "$TRAVIS_ORACLE_URL" \ 14 | | tar -xz --strip-components 1 -C .travis/oracle 15 | 16 | .travis/oracle/download.sh 17 | .travis/oracle/install.sh 18 | 19 | "$ORACLE_HOME"/bin/sqlplus -L -S / AS SYSDBA <<-SQL 20 | CREATE USER test IDENTIFIED BY test; 21 | GRANT CONNECT, RESOURCE TO test; 22 | SQL 23 | 24 | mvn install:install-file \ 25 | -DgroupId='com.oracle' \ 26 | -DartifactId='ojdbc6' \ 27 | -Dversion='11.2.0.3' \ 28 | -Dpackaging='jar' \ 29 | -Dfile="$ORACLE_HOME/jdbc/lib/ojdbc6.jar" \ 30 | -DgeneratePom=true 31 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/RowUnmapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc; 17 | 18 | import java.util.Map; 19 | 20 | public interface RowUnmapper { 21 | 22 | Map mapColumns(T t); 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/schema_mssql.sql: -------------------------------------------------------------------------------- 1 | -- Schema can be used for MS SQLServer 2008 and 2012 2 | 3 | IF OBJECT_ID('USERS', 'U') IS NOT NULL 4 | DROP TABLE USERS; 5 | 6 | CREATE TABLE USERS ( 7 | user_name VARCHAR(255) PRIMARY KEY, 8 | date_of_birth DATE NOT NULL, --timestamp columns can not be used for explicit inserts 9 | reputation INT NOT NULL, 10 | enabled BIT NOT NULL 11 | ); 12 | 13 | IF OBJECT_ID('COMMENTS', 'U') IS NOT NULL 14 | DROP TABLE COMMENTS; 15 | 16 | CREATE TABLE COMMENTS ( 17 | id INT IDENTITY (1, 1) PRIMARY KEY, 18 | user_name VARCHAR(256), 19 | contents VARCHAR(1000), 20 | created_time DATETIME NOT NULL, --timestamp columns can not be used for explicit inserts 21 | favourite_count INT NOT NULL 22 | ); 23 | 24 | IF OBJECT_ID('BOARDING_PASS', 'U') IS NOT NULL 25 | DROP TABLE BOARDING_PASS; 26 | 27 | CREATE TABLE BOARDING_PASS ( 28 | flight_no VARCHAR(8) NOT NULL, 29 | seq_no INT NOT NULL, 30 | passenger VARCHAR(1000), 31 | seat CHAR(3) 32 | CONSTRAINT PK_BOARDING_PASS PRIMARY KEY CLUSTERED (flight_no, seq_no) 33 | ); 34 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | clone_depth: 10 3 | skip_tags: true 4 | 5 | os: Windows Server 2012 6 | services: 7 | - mssql2012sp1 8 | - mssql2014 9 | environment: 10 | global: 11 | DB: mssql 12 | matrix: 13 | - MSSQL_INSTANCE: SQL2012SP1 14 | JAVA_HOME: C:\Program Files\Java\jdk1.7.0 15 | - MSSQL_INSTANCE: SQL2012SP1 16 | JAVA_HOME: C:\Program Files\Java\jdk1.8.0 17 | - MSSQL_INSTANCE: SQL2014 18 | JAVA_HOME: C:\Program Files\Java\jdk1.7.0 19 | - MSSQL_INSTANCE: SQL2014 20 | JAVA_HOME: C:\Program Files\Java\jdk1.8.0 21 | 22 | install: 23 | - ps: .\script\appveyor-setup-mssql.ps1 $env:MSSQL_INSTANCE 24 | - ps: .\script\appveyor-install-maven.ps1 3.3.9 25 | - cmd: mvn --version 26 | - cmd: java -version 27 | 28 | before_build: 29 | - sqlcmd -U sa -P Password12! -S .\%MSSQL_INSTANCE% -Q "CREATE DATABASE spring_data_jdbc_repository_test" 30 | - sqlcmd -U sa -P Password12! -S .\%MSSQL_INSTANCE% -d spring_data_jdbc_repository_test -i src\test\resources\schema_mssql.sql 31 | build_script: 32 | - mvn clean install -DskipTests=true 33 | test_script: 34 | - mvn verify -B 35 | cache: 36 | - C:\maven\ 37 | - C:\Users\appveyor\.m2 38 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/TestUtils.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | abstract class TestUtils { 19 | 20 | static boolean isPortInUse(String host, int port) { 21 | try { 22 | new Socket(host, port).withCloseable { 23 | // do nothing 24 | } 25 | true 26 | } catch (IOException ex) { 27 | false 28 | } 29 | } 30 | 31 | static String env(String name, String defaultValue = null) { 32 | System.getenv(name) ?: defaultValue 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/resources/schema_oracle.sql: -------------------------------------------------------------------------------- 1 | DROP SEQUENCE comment_seq; 2 | DROP TABLE COMMENTS; 3 | DROP TABLE users; 4 | DROP TABLE BOARDING_PASS; 5 | 6 | 7 | CREATE TABLE USERS ( 8 | user_name VARCHAR(255), 9 | date_of_birth DATE NOT NULL, 10 | reputation INT NOT NULL, 11 | enabled INT NOT NULL, 12 | CONSTRAINT pk_users_user_name PRIMARY KEY (user_name) 13 | ); 14 | 15 | 16 | CREATE SEQUENCE comment_seq 17 | START WITH 1000 18 | INCREMENT BY 1 19 | NOCACHE 20 | ; 21 | 22 | CREATE TABLE COMMENTS ( 23 | id INT, 24 | user_name VARCHAR(256) REFERENCES USERS, 25 | contents VARCHAR(1000), 26 | created_time TIMESTAMP NOT NULL, 27 | favourite_count INT NOT NULL, 28 | CONSTRAINT pk_comment_id PRIMARY KEY (id) 29 | ); 30 | 31 | CREATE OR REPLACE TRIGGER COMMENT_ID_GEN 32 | BEFORE INSERT ON COMMENTS 33 | FOR EACH ROW 34 | BEGIN 35 | SELECT 36 | comment_seq.nextval 37 | INTO :new.id 38 | FROM dual; 39 | END; 40 | / 41 | 42 | 43 | CREATE TABLE BOARDING_PASS ( 44 | flight_no VARCHAR(8) NOT NULL, 45 | seq_no INT NOT NULL, 46 | passenger VARCHAR(1000), 47 | seat CHAR(3), 48 | CONSTRAINT pk_BOARDING_PASS_fn_sn PRIMARY KEY (flight_no, seq_no) 49 | ); 50 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/internal/IterableUtilsTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.internal 17 | 18 | import spock.lang.Specification 19 | 20 | import static cz.jirutka.spring.data.jdbc.internal.IterableUtils.toList 21 | 22 | class IterableUtilsTest extends Specification { 23 | 24 | 25 | def 'toList(): converts given iterable into a list'() { 26 | given: 27 | def input = type.newInstance([1, 2, 3]) as Iterable 28 | expect: 29 | toList(input) == [1, 2, 3] 30 | where: 31 | type << [ArrayList, PriorityQueue, HashSet] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/UnsupportedRowUnmapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc; 18 | 19 | import java.util.Map; 20 | 21 | /** 22 | * No-operational implementation of {@link RowUnmapper} that just 23 | * throws {@link UnsupportedOperationException}. 24 | */ 25 | public class UnsupportedRowUnmapper implements RowUnmapper { 26 | 27 | public Map mapColumns(T o) { 28 | throw new UnsupportedOperationException( 29 | "This repository is read-only, it can't store or update entities"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/sql/LimitOffsetSqlGeneratorTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.sql 17 | 18 | import cz.jirutka.spring.data.jdbc.TableDescription 19 | import org.springframework.data.domain.Pageable 20 | 21 | class LimitOffsetSqlGeneratorTest extends SqlGeneratorTest { 22 | 23 | def sqlGenerator = new LimitOffsetSqlGenerator() 24 | 25 | 26 | @Override expectedPaginatedQuery(TableDescription table, Pageable page) { 27 | def orderBy = page.sort ? orderBy(page.sort) + ' ' : '' 28 | 29 | "SELECT a, b FROM tabx ${orderBy}LIMIT ${page.pageSize} OFFSET ${page.offset}" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/internal/IterableUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.internal; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | public final class IterableUtils { 22 | 23 | private IterableUtils() {} 24 | 25 | /** 26 | * Converts the given Iterable into an ArrayList. 27 | */ 28 | public static List toList(Iterable iterable) { 29 | 30 | if (iterable instanceof List) { 31 | return (List) iterable; 32 | } 33 | 34 | List result = new ArrayList<>(); 35 | for (T item : iterable) { 36 | result.add(item); 37 | } 38 | 39 | return result; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /script/appveyor-setup-mssql.ps1: -------------------------------------------------------------------------------- 1 | # Source: https://gist.github.com/FeodorFitsner/d971c5a98782d211640d 2 | # I have no clue what this mess actually do... 3 | 4 | [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null 5 | [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.SqlWmiManagement") | Out-Null 6 | 7 | $instanceName = $args[0] 8 | $serverName = $env:COMPUTERNAME 9 | $smo = 'Microsoft.SqlServer.Management.Smo.' 10 | $wmi = new-object ($smo + 'Wmi.ManagedComputer') 11 | 12 | echo "Enabling TCP/IP for $instanceName" 13 | $uri = "ManagedComputer[@Name='$serverName']/ServerInstance[@Name='$instanceName']/ServerProtocol[@Name='Tcp']" 14 | $Tcp = $wmi.GetSmoObject($uri) 15 | $Tcp.IsEnabled = $true 16 | foreach ($ipAddress in $Tcp.IPAddresses) { 17 | $ipAddress.IPAddressProperties["TcpDynamicPorts"].Value = "" 18 | $ipAddress.IPAddressProperties["TcpPort"].Value = "1433" 19 | } 20 | $Tcp.Alter() 21 | 22 | echo "Enabling named pipes for $instanceName" 23 | $uri = "ManagedComputer[@Name='$serverName']/ServerInstance[@Name='$instanceName']/ServerProtocol[@Name='Np']" 24 | $Np = $wmi.GetSmoObject($uri) 25 | $Np.IsEnabled = $true 26 | $Np.Alter() 27 | 28 | echo "Setting alias for $instanceName" 29 | New-Item HKLM:\SOFTWARE\Microsoft\MSSQLServer\Client -Name ConnectTo | Out-Null 30 | Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\MSSQLServer\Client\ConnectTo -Name '(local)' -Value "DBMSSOCN,$serverName\$instanceName" | Out-Null 31 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/fixtures/CommentWithUser.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.fixtures 18 | 19 | import groovy.transform.EqualsAndHashCode 20 | import groovy.transform.ToString 21 | 22 | @ToString 23 | @EqualsAndHashCode 24 | class CommentWithUser extends Comment { 25 | 26 | User user 27 | 28 | 29 | CommentWithUser(User user, String contents, Date createdTime, int favouriteCount) { 30 | this(null, user, contents, createdTime, favouriteCount) 31 | } 32 | 33 | CommentWithUser(Integer id, User user, String contents, Date createdTime, int favouriteCount) { 34 | super(id, user.userName, contents, createdTime, favouriteCount) 35 | this.user = user 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/sql/SqlGeneratorFactoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.sql 17 | 18 | import org.springframework.beans.factory.annotation.Autowired 19 | import spock.lang.Specification 20 | import spock.lang.Unroll 21 | 22 | import javax.sql.DataSource 23 | 24 | @Unroll 25 | abstract class SqlGeneratorFactoryIT extends Specification { 26 | 27 | @Autowired DataSource dataSource 28 | 29 | 30 | abstract Class getExpectedGenerator() 31 | 32 | 33 | def 'getGenerator(): returns #generatorClass'() { 34 | when: 35 | def generator = SqlGeneratorFactory.instance.getGenerator(dataSource) 36 | then: 37 | generator.class == expectedGenerator 38 | where: 39 | generatorClass = expectedGenerator.simpleName 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/internal/StringUtilsTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.internal 17 | 18 | import spock.lang.Specification 19 | import spock.lang.Unroll 20 | 21 | import static cz.jirutka.spring.data.jdbc.internal.StringUtils.repeat 22 | 23 | @Unroll 24 | class StringUtilsTest extends Specification { 25 | 26 | def 'repeat("#str", "#sep", #count) == "#expected"'() { 27 | expect: 28 | repeat(str, sep, count) == expected 29 | where: 30 | str | sep | count || expected 31 | 'x' | ' or ' | 3 || 'x or x or x' 32 | 'x' | ' or ' | 1 || 'x' 33 | 'x' | ' or ' | 0 || '' 34 | 'x' | ' or ' | -2 || '' 35 | 'pew' | '' | 2 || 'pewpew' 36 | '' | '-' | 2 || '-' 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/fixtures/Comment.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.fixtures 18 | 19 | import groovy.transform.EqualsAndHashCode 20 | import groovy.transform.ToString 21 | import org.springframework.data.annotation.Id 22 | 23 | @ToString 24 | @EqualsAndHashCode 25 | class Comment { 26 | 27 | @Id 28 | Integer id 29 | 30 | String userName 31 | 32 | String contents 33 | 34 | Date createdTime 35 | 36 | int favouriteCount 37 | 38 | 39 | Comment(Integer id, String userName, String contents, Date createdTime, int favouriteCount) { 40 | this.id = id 41 | this.userName = userName 42 | this.contents = contents 43 | this.createdTime = createdTime 44 | this.favouriteCount = favouriteCount 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/internal/ObjectUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.internal; 17 | 18 | import static org.springframework.util.ObjectUtils.toObjectArray; 19 | 20 | public final class ObjectUtils { 21 | 22 | private ObjectUtils() {} 23 | 24 | /** 25 | * Wraps the given object into an object array. If the object is an object 26 | * array, then it returns it as-is. If it's an array of primitives, then 27 | * it converts it into an array of primitive wrapper objects. 28 | */ 29 | public static Object[] wrapToArray(Object obj) { 30 | if (obj == null) { 31 | return new Object[0]; 32 | } 33 | if (obj instanceof Object[]) { 34 | return (Object[]) obj; 35 | } 36 | if (obj.getClass().isArray()) { 37 | return toObjectArray(obj); 38 | } 39 | return new Object[]{ obj }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/internal/ObjectUtilsTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.internal 17 | 18 | import spock.lang.Specification 19 | import spock.lang.Unroll 20 | 21 | import static cz.jirutka.spring.data.jdbc.internal.ObjectUtils.wrapToArray 22 | 23 | @Unroll 24 | class ObjectUtilsTest extends Specification { 25 | 26 | 27 | def 'wrapToArray(): returns #expected for #desc'() { 28 | expect: 29 | wrapToArray(input) == expected 30 | where: 31 | input | expected || desc 32 | null | [] || 'null' 33 | 'foo' | ['foo'] || 'a single object value' 34 | 42 | [42] || 'a single primitive value' 35 | ['a', 'b'] as Object[] | ['a', 'b'] || 'an array of objects' 36 | [1, 2] as int[] | [1, 2] || 'an array of primitives' 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/sql/SQL2008SqlGeneratorTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.sql 17 | 18 | import cz.jirutka.spring.data.jdbc.TableDescription 19 | import org.springframework.data.domain.Pageable 20 | import org.springframework.data.domain.Sort 21 | 22 | import static org.springframework.data.domain.Sort.Direction.ASC 23 | 24 | class SQL2008SqlGeneratorTest extends SqlGeneratorTest { 25 | 26 | def sqlGenerator = new SQL2008SqlGenerator() 27 | 28 | 29 | @Override 30 | expectedPaginatedQuery(TableDescription table, Pageable page) { 31 | 32 | // If sort is not specified, then it should be sorted by primary key columns. 33 | def sort = page.sort ?: new Sort(ASC, table.pkColumns) 34 | 35 | """ 36 | SELECT a, b FROM tabx ${orderBy(sort)} 37 | OFFSET ${page.offset} ROWS FETCH NEXT ${page.pageSize} ROW ONLY 38 | """.trim().replaceAll(/\s+/, ' ') 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: java 3 | jdk: 4 | - openjdk7 5 | - oraclejdk8 6 | env: 7 | global: 8 | - PROFILE=default 9 | - ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe 10 | - ORACLE_SID=XE 11 | - secure: DGNJTv3zKzaP6XdPIioZKyhdBCH/R4Ngbtkx61nONq5mLFUX4X9X1kFBRon7B7k+vuTbTsThE+Rt7L78hogwChkU7cG39/OJpNmLCAN7rQmb2QFln9fwygGKqYVza8vabYgIpM9z6pHr+miFKAxYaO8JqWghgMJKj8lhogRxZs4= # ORACLE_LOGIN_ssousername 12 | - secure: w9IL1XjC3SWZ/6YD0qPsNxCYWvP9+zLMViUj+9JPyAaQe80n0uwWXfn1jBfoiBm1+69Qq4UjVbcxYLueIWeuHhRI3ZdvyZlw+DahgRzLviHEjJXTN7E+//UeoKMCSVFAxfn9jK3CuSDQ6T7IymhRXHC5uLvk/MHJLrv2QbHceyw= # ORACLE_LOGIN_password 13 | - secure: nyniezePeWzxbIS2+DGqhVRbRlkTcvFAkh7KDG46Y9gwDloc9U6qvGcsLRfhHPVCv/r0g3OROUUBhIs4H+Malm1MTgXGBHwbIy62BnFWTQdwd6DQr7sclkxFjFHxPQ0aewObI4Fw70nwJKNnJosRZApmaZgYRwvTwWt5nc6jGXc= # BINTRAY_API_KEY 14 | matrix: 15 | - DB=embedded 16 | - DB=postgresql 17 | - DB=mysql 18 | matrix: 19 | include: 20 | - env: DB=mariadb 21 | jdk: openjdk7 22 | addons: 23 | mariadb: '10.1' 24 | - env: DB=mariadb 25 | jdk: oraclejdk8 26 | addons: 27 | mariadb: '10.1' 28 | - env: DB=oracle PROFILE=oracle 29 | jdk: openjdk7 30 | sudo: required 31 | - env: DB=oracle PROFILE=oracle 32 | jdk: oraclejdk8 33 | sudo: required 34 | 35 | cache: 36 | directories: 37 | - $HOME/.m2 38 | before_cache: 39 | - rm -Rf $HOME/.m2/repository/cz/jirutka/spring/spring-data-jdbc-repository 40 | 41 | before_script: 42 | - script/travis-setup 43 | install: 44 | - mvn clean install -DskipTests=true 45 | script: 46 | - mvn verify -B -P $PROFILE 47 | after_success: 48 | - script/travis-deploy 49 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/internal/StringUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.internal; 17 | 18 | import static java.lang.Math.max; 19 | 20 | public final class StringUtils { 21 | 22 | private StringUtils() {} 23 | 24 | /** 25 | * Repeats the given String {@code count}-times to form a new String, with 26 | * the {@code separator} injected between. 27 | * 28 | * @param str The string to repeat. 29 | * @param separator The string to inject between. 30 | * @param count Number of times to repeat {@code str}; negative treated 31 | * as zero. 32 | * @return A new String. 33 | */ 34 | public static String repeat(String str, String separator, int count) { 35 | StringBuilder sb = new StringBuilder((str.length() + separator.length()) * max(count, 0)); 36 | 37 | for (int n = 0; n < count; n++) { 38 | if (n > 0) sb.append(separator); 39 | sb.append(str); 40 | } 41 | return sb.toString(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/BaseJdbcRepositoryTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import cz.jirutka.spring.data.jdbc.fixtures.Comment 19 | import cz.jirutka.spring.data.jdbc.fixtures.CommentRepository 20 | import cz.jirutka.spring.data.jdbc.fixtures.User 21 | import cz.jirutka.spring.data.jdbc.fixtures.UserRepository 22 | import spock.lang.Specification 23 | import spock.lang.Unroll 24 | 25 | @Unroll 26 | class BaseJdbcRepositoryTest extends Specification { 27 | 28 | def 'constructor: creates correct default EntityInformation for #desc'() { 29 | setup: 30 | def entityInfo = fixtureRepo.newInstance().entityInfo 31 | expect: 32 | entityInfo.javaType == entityType 33 | entityInfo.idType == idType 34 | where: 35 | fixtureRepo || entityType | idType || desc 36 | UserRepository || User | String || 'Persistable entity type' 37 | CommentRepository || Comment | Integer || 'non-Persistable entity type' 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/fixtures/User.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.fixtures 18 | 19 | import groovy.transform.EqualsAndHashCode 20 | import groovy.transform.ToString 21 | import org.springframework.data.domain.Persistable 22 | 23 | @ToString 24 | @EqualsAndHashCode 25 | class User implements Persistable { 26 | 27 | private transient boolean persisted 28 | 29 | String userName 30 | 31 | Date dateOfBirth 32 | 33 | int reputation 34 | 35 | boolean enabled 36 | 37 | 38 | User(String userName, Date dateOfBirth, int reputation, boolean enabled) { 39 | this.userName = userName 40 | this.dateOfBirth = dateOfBirth 41 | this.reputation = reputation 42 | this.enabled = enabled 43 | } 44 | 45 | 46 | String getId() { 47 | userName 48 | } 49 | 50 | boolean isNew() { 51 | !persisted 52 | } 53 | 54 | User withPersisted(boolean persisted) { 55 | this.persisted = persisted 56 | this 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/sql/Oracle9SqlGeneratorTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.sql 17 | 18 | import cz.jirutka.spring.data.jdbc.TableDescription 19 | import org.springframework.data.domain.Pageable 20 | import org.springframework.data.domain.Sort 21 | 22 | import static org.springframework.data.domain.Sort.Direction.ASC 23 | 24 | class Oracle9SqlGeneratorTest extends SqlGeneratorTest { 25 | 26 | def sqlGenerator = new Oracle9SqlGenerator() 27 | 28 | 29 | @Override expectedPaginatedQuery(TableDescription table, Pageable page) { 30 | 31 | // If sort is not specified, then it should be sorted by primary key columns. 32 | def sort = page.sort ?: new Sort(ASC, table.pkColumns) 33 | 34 | """ 35 | SELECT t2__.* FROM ( 36 | SELECT t1__.*, ROWNUM as rn__ FROM ( 37 | SELECT ${table.selectClause} FROM ${table.fromClause} ${orderBy(sort)} 38 | ) t1__ 39 | ) t2__ WHERE t2__.rn__ > ${page.offset} AND ROWNUM <= ${page.pageSize} 40 | """.trim().replaceAll(/\s+/, ' ') 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/fixtures/BoardingPass.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.fixtures 18 | 19 | import groovy.transform.EqualsAndHashCode 20 | import groovy.transform.ToString 21 | import org.springframework.data.domain.Persistable 22 | 23 | @ToString 24 | @EqualsAndHashCode 25 | class BoardingPass implements Persistable { 26 | 27 | private transient boolean persisted 28 | 29 | String flightNo 30 | 31 | int seqNo 32 | 33 | String passenger 34 | 35 | String seat 36 | 37 | 38 | BoardingPass() { 39 | } 40 | 41 | BoardingPass(String flightNo, int seqNo, String passenger, String seat) { 42 | this.flightNo = flightNo 43 | this.seqNo = seqNo 44 | this.passenger = passenger 45 | this.seat = seat 46 | } 47 | 48 | 49 | Object[] getId() { 50 | [flightNo, seqNo] 51 | } 52 | 53 | boolean isNew() { 54 | !persisted 55 | } 56 | 57 | BoardingPass withPersisted(boolean persisted) { 58 | this.persisted = persisted 59 | this 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/sql/LimitOffsetSqlGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.sql; 17 | 18 | import cz.jirutka.spring.data.jdbc.TableDescription; 19 | import org.springframework.data.domain.Pageable; 20 | 21 | import java.sql.DatabaseMetaData; 22 | import java.sql.SQLException; 23 | import java.util.List; 24 | 25 | import static java.lang.String.format; 26 | import static java.util.Arrays.asList; 27 | 28 | /** 29 | * SQL Generator for DB servers that support LIMIT ... OFFSET clause: 30 | * PostgreSQL, H2, HSQLDB, SQLite, MariaDB, and MySQL. 31 | */ 32 | public class LimitOffsetSqlGenerator extends DefaultSqlGenerator { 33 | 34 | private static final List SUPPORTED_PRODUCTS = 35 | asList("PostgreSQL", "H2", "HSQL Database Engine", "MySQL"); 36 | 37 | 38 | @Override 39 | public boolean isCompatible(DatabaseMetaData metadata) throws SQLException { 40 | return SUPPORTED_PRODUCTS.contains(metadata.getDatabaseProductName()); 41 | } 42 | 43 | @Override 44 | public String selectAll(TableDescription table, Pageable page) { 45 | return format("%s LIMIT %d OFFSET %d", 46 | selectAll(table, page.getSort()), page.getPageSize(), page.getOffset()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/sql/Oracle9SqlGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.sql; 17 | 18 | import cz.jirutka.spring.data.jdbc.TableDescription; 19 | import org.springframework.data.domain.Pageable; 20 | import org.springframework.data.domain.Sort; 21 | 22 | import java.sql.DatabaseMetaData; 23 | import java.sql.SQLException; 24 | 25 | import static java.lang.String.format; 26 | 27 | /** 28 | * SQL Generator for Oracle up to 11g. If you have 12g or newer, then use 29 | * {@link SQL2008SqlGenerator}. 30 | * 31 | * @see 32 | * Oracle: ROW_NUMBER vs ROWNUM 33 | */ 34 | public class Oracle9SqlGenerator extends DefaultSqlGenerator { 35 | 36 | @Override 37 | public boolean isCompatible(DatabaseMetaData metadata) throws SQLException { 38 | return "Oracle".equals(metadata.getDatabaseProductName()); 39 | } 40 | 41 | @Override 42 | public String selectAll(TableDescription table, Pageable page) { 43 | Sort sort = page.getSort() != null ? page.getSort() : sortById(table); 44 | 45 | return format("SELECT t2__.* FROM ( " 46 | + "SELECT t1__.*, ROWNUM as rn__ FROM ( %s ) t1__ " 47 | + ") t2__ WHERE t2__.rn__ > %d AND ROWNUM <= %d", 48 | selectAll(table, sort), page.getOffset(), page.getPageSize()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/sql/SQL2008SqlGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.sql; 17 | 18 | import cz.jirutka.spring.data.jdbc.TableDescription; 19 | import org.springframework.data.domain.Pageable; 20 | import org.springframework.data.domain.Sort; 21 | 22 | import java.sql.DatabaseMetaData; 23 | import java.sql.SQLException; 24 | 25 | import static java.lang.String.format; 26 | 27 | /** 28 | * SQL Generator for DB servers that support the SQL:2008 standard OFFSET 29 | * feature: Apache Derby, Microsoft SQL Server 2012, and Oracle 12c. 30 | */ 31 | public class SQL2008SqlGenerator extends DefaultSqlGenerator { 32 | 33 | @Override 34 | public boolean isCompatible(DatabaseMetaData metadata) throws SQLException { 35 | String productName = metadata.getDatabaseProductName(); 36 | int majorVersion = metadata.getDatabaseMajorVersion(); 37 | 38 | return "Apache Derby".equals(productName) 39 | || "Oracle".equals(productName) && majorVersion >= 12 40 | || "Microsoft SQL Server".equals(productName) && majorVersion >= 11; // >= 2012 41 | } 42 | 43 | @Override 44 | public String selectAll(TableDescription table, Pageable page) { 45 | Sort sort = page.getSort() != null ? page.getSort() : sortById(table); 46 | 47 | return format("%s OFFSET %d ROWS FETCH NEXT %d ROW ONLY", 48 | selectAll(table, sort), page.getOffset(), page.getPageSize()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/NoRecordUpdatedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc; 17 | 18 | import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; 19 | 20 | import static java.lang.String.format; 21 | import static org.springframework.util.StringUtils.arrayToCommaDelimitedString; 22 | 23 | /** 24 | * Exception thrown when trying to update a record that doesn't exist. 25 | */ 26 | public class NoRecordUpdatedException extends IncorrectUpdateSemanticsDataAccessException { 27 | 28 | private final String tableName; 29 | private final Object[] id; 30 | 31 | 32 | public NoRecordUpdatedException(String tableName, Object... id) { 33 | super(format("No record with id = {%s} exists in table %s", 34 | arrayToCommaDelimitedString(id), tableName)); 35 | this.tableName = tableName; 36 | this.id = id.clone(); 37 | } 38 | 39 | public NoRecordUpdatedException(String tableName, String msg) { 40 | super(msg); 41 | this.tableName = tableName; 42 | this.id = new Object[0]; 43 | } 44 | 45 | public NoRecordUpdatedException(String tableName, String msg, Throwable cause) { 46 | super(msg, cause); 47 | this.tableName = tableName; 48 | this.id = new Object[0]; 49 | } 50 | 51 | 52 | @Override 53 | public boolean wasDataUpdated() { 54 | return false; 55 | } 56 | 57 | public String getTableName() { 58 | return tableName; 59 | } 60 | 61 | public Object[] getId() { 62 | return id.clone(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/StandaloneUsageIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License') 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc 18 | 19 | import cz.jirutka.spring.data.jdbc.fixtures.User 20 | import cz.jirutka.spring.data.jdbc.fixtures.UserRepository 21 | import org.h2.jdbcx.JdbcDataSource 22 | import org.springframework.jdbc.datasource.DataSourceTransactionManager 23 | import org.springframework.transaction.TransactionStatus 24 | import org.springframework.transaction.support.TransactionTemplate 25 | import spock.lang.Specification 26 | 27 | class StandaloneUsageIT extends Specification { 28 | 29 | final JDBC_URL = "jdbc:h2:mem:DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'classpath:schema_h2.sql'" 30 | 31 | def dataSource = new JdbcDataSource(url: JDBC_URL) 32 | def repository = new UserRepository(dataSource: dataSource) 33 | 34 | 35 | def setup() { 36 | repository.afterPropertiesSet() 37 | } 38 | 39 | def cleanup() { 40 | repository.deleteAll() 41 | } 42 | 43 | 44 | def 'start repository without Spring'() { 45 | expect: 46 | repository.findAll().isEmpty() 47 | } 48 | 49 | def 'insert into database'() { 50 | given: 51 | def tx = new TransactionTemplate(new DataSourceTransactionManager(dataSource)) 52 | when: 53 | def users = tx.execute { TransactionStatus status -> 54 | def user = new User('john', new Date(), 0, false) 55 | repository.save(user) 56 | repository.findAll() 57 | } 58 | then: 59 | users.size() == 1 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/sql/SqlGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.sql; 18 | 19 | import cz.jirutka.spring.data.jdbc.TableDescription; 20 | import org.springframework.data.domain.Pageable; 21 | import org.springframework.data.domain.Sort; 22 | 23 | import java.sql.DatabaseMetaData; 24 | import java.sql.SQLException; 25 | import java.util.Map; 26 | 27 | public interface SqlGenerator { 28 | 29 | /** 30 | * This method is used by {@link SqlGeneratorFactory} to select a right 31 | * SQL Generator. 32 | * 33 | * @param metadata The database metadata. 34 | * @return Whether is this generator compatible with the database described 35 | * by the given {@code metadata}. 36 | */ 37 | boolean isCompatible(DatabaseMetaData metadata) throws SQLException; 38 | 39 | 40 | String count(TableDescription table); 41 | 42 | String deleteAll(TableDescription table); 43 | 44 | String deleteById(TableDescription table); 45 | 46 | String deleteByIds(TableDescription table, int idsCount); 47 | 48 | String existsById(TableDescription table); 49 | 50 | String insert(TableDescription table, Map columns); 51 | 52 | String selectAll(TableDescription table); 53 | 54 | String selectAll(TableDescription table, Pageable page); 55 | 56 | String selectAll(TableDescription table, Sort sort); 57 | 58 | String selectById(TableDescription table); 59 | 60 | String selectByIds(TableDescription table, int idsCount); 61 | 62 | String update(TableDescription table, Map columns); 63 | } 64 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/MssqlJdbcRepositoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import cz.jirutka.spring.data.jdbc.fixtures.CommentWithUserRepository 19 | import groovy.transform.AnnotationCollector 20 | import org.springframework.context.annotation.Configuration 21 | import org.springframework.test.context.ContextConfiguration 22 | import org.springframework.transaction.annotation.EnableTransactionManagement 23 | import spock.lang.Requires 24 | 25 | import static MssqlTestConfig.MSSQL_HOST 26 | import static TestUtils.env 27 | import static TestUtils.isPortInUse 28 | 29 | @MssqlTestContext 30 | class MssqlJdbcRepositoryCompoundPkIT extends JdbcRepositoryCompoundPkIT {} 31 | 32 | @MssqlTestContext 33 | class MssqlJdbcRepositoryGeneratedKeyIT extends JdbcRepositoryGeneratedKeyIT {} 34 | 35 | @MssqlTestContext 36 | class MssqlJdbcRepositoryManualKeyIT extends JdbcRepositoryManualKeyIT {} 37 | 38 | @MssqlTestContext 39 | class MssqlJdbcRepositoryManyToOneIT extends JdbcRepositoryManyToOneIT {} 40 | 41 | @AnnotationCollector 42 | @Requires({ env('CI') ? env('DB').equals('mssql') : isPortInUse(MSSQL_HOST, 1433) }) 43 | @ContextConfiguration(classes = MssqlTestConfig) 44 | @interface MssqlTestContext {} 45 | 46 | @Configuration 47 | @EnableTransactionManagement 48 | class MssqlTestConfig extends Mssql2012TestConfig { 49 | 50 | @Override CommentWithUserRepository commentWithUserRepository() { 51 | new CommentWithUserRepository( 52 | new TableDescription( 53 | tableName: 'COMMENTS', 54 | selectClause: 'c.*, u.date_of_birth, u.reputation, u.enabled', 55 | fromClause: 'COMMENTS c JOIN USERS u ON c.USER_NAME = u.USER_NAME', 56 | pkColumns: ['ID'] 57 | ) 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/H2JdbcRepositoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import cz.jirutka.spring.data.jdbc.config.AbstractTestConfig 19 | import cz.jirutka.spring.data.jdbc.sql.LimitOffsetSqlGenerator 20 | import cz.jirutka.spring.data.jdbc.sql.SqlGeneratorFactoryIT 21 | import groovy.transform.AnnotationCollector 22 | import org.h2.jdbcx.JdbcDataSource 23 | import org.springframework.context.annotation.Bean 24 | import org.springframework.context.annotation.Configuration 25 | import org.springframework.test.context.ContextConfiguration 26 | import org.springframework.transaction.annotation.EnableTransactionManagement 27 | import spock.lang.Requires 28 | 29 | import javax.sql.DataSource 30 | 31 | import static TestUtils.env 32 | 33 | @H2TestContext 34 | class H2JdbcRepositoryCompoundPkIT extends JdbcRepositoryCompoundPkIT {} 35 | 36 | @H2TestContext 37 | class H2JdbcRepositoryGeneratedKeyIT extends JdbcRepositoryGeneratedKeyIT {} 38 | 39 | @H2TestContext 40 | class H2JdbcRepositoryManualKeyIT extends JdbcRepositoryManualKeyIT {} 41 | 42 | @H2TestContext 43 | class H2JdbcRepositoryManyToOneIT extends JdbcRepositoryManyToOneIT {} 44 | 45 | @H2TestContext 46 | class H2SqlGeneratorFactoryIT extends SqlGeneratorFactoryIT { 47 | Class getExpectedGenerator() { LimitOffsetSqlGenerator } 48 | } 49 | 50 | @AnnotationCollector 51 | @Requires({ env('CI') ? env('DB').equals('embedded') : true }) 52 | @ContextConfiguration(classes = H2TestConfig) 53 | @interface H2TestContext {} 54 | 55 | @Configuration 56 | @EnableTransactionManagement 57 | class H2TestConfig extends AbstractTestConfig { 58 | 59 | @Bean DataSource dataSource() { 60 | new JdbcDataSource ( 61 | url: "jdbc:h2:mem:DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'classpath:schema_h2.sql'" 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/HsqldbJdbcRepositoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import cz.jirutka.spring.data.jdbc.config.AbstractTestConfig 19 | import cz.jirutka.spring.data.jdbc.sql.LimitOffsetSqlGenerator 20 | import cz.jirutka.spring.data.jdbc.sql.SqlGeneratorFactoryIT 21 | import groovy.transform.AnnotationCollector 22 | import org.springframework.context.annotation.Bean 23 | import org.springframework.context.annotation.Configuration 24 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder 25 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType 26 | import org.springframework.test.context.ContextConfiguration 27 | import org.springframework.transaction.annotation.EnableTransactionManagement 28 | import spock.lang.Requires 29 | 30 | import javax.sql.DataSource 31 | 32 | import static TestUtils.env 33 | 34 | @HsqldbTestContext 35 | class HsqldbJdbcRepositoryCompoundPkIT extends JdbcRepositoryCompoundPkIT {} 36 | 37 | @HsqldbTestContext 38 | class HsqldbJdbcRepositoryGeneratedKeyIT extends JdbcRepositoryGeneratedKeyIT {} 39 | 40 | @HsqldbTestContext 41 | class HsqldbJdbcRepositoryManualKeyIT extends JdbcRepositoryManualKeyIT {} 42 | 43 | @HsqldbTestContext 44 | class HsqldbJdbcRepositoryManyToOneIT extends JdbcRepositoryManyToOneIT {} 45 | 46 | @HsqldbTestContext 47 | class HsqldbSqlGeneratorFactoryIT extends SqlGeneratorFactoryIT { 48 | Class getExpectedGenerator() { LimitOffsetSqlGenerator } 49 | } 50 | 51 | @AnnotationCollector 52 | @Requires({ env('CI') ? env('DB').equals('embedded') : true }) 53 | @ContextConfiguration(classes = HsqldbTestConfig) 54 | @interface HsqldbTestContext {} 55 | 56 | @Configuration 57 | @EnableTransactionManagement 58 | class HsqldbTestConfig extends AbstractTestConfig { 59 | 60 | @Bean DataSource dataSource() { 61 | new EmbeddedDatabaseBuilder() 62 | .addScript('schema_hsqldb.sql') 63 | .setType(EmbeddedDatabaseType.H2) 64 | .build() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/cz/jirutka/spring/data/jdbc/fixtures/BoardingPassRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.fixtures; 18 | 19 | import cz.jirutka.spring.data.jdbc.BaseJdbcRepository; 20 | import cz.jirutka.spring.data.jdbc.RowUnmapper; 21 | import cz.jirutka.spring.data.jdbc.TableDescription; 22 | import org.springframework.jdbc.core.RowMapper; 23 | 24 | import java.sql.ResultSet; 25 | import java.sql.SQLException; 26 | import java.util.HashMap; 27 | import java.util.Map; 28 | 29 | public class BoardingPassRepository extends BaseJdbcRepository { 30 | 31 | public static final RowMapper ROW_MAPPER = new RowMapper() { 32 | 33 | public BoardingPass mapRow(ResultSet rs, int rowNum) throws SQLException { 34 | BoardingPass boardingPass = new BoardingPass( 35 | rs.getString("flight_no"), 36 | rs.getInt("seq_no"), 37 | rs.getString("passenger"), 38 | rs.getString("seat") 39 | ); 40 | return boardingPass.withPersisted(true); 41 | } 42 | }; 43 | 44 | public static final RowUnmapper ROW_UNMAPPER = new RowUnmapper() { 45 | 46 | public Map mapColumns(BoardingPass o) { 47 | HashMap row = new HashMap<>(); 48 | row.put("flight_no", o.getFlightNo()); 49 | row.put("seq_no", o.getSeqNo()); 50 | row.put("passenger", o.getPassenger()); 51 | row.put("seat", o.getSeat()); 52 | return row; 53 | } 54 | }; 55 | 56 | 57 | public BoardingPassRepository() { 58 | super(ROW_MAPPER, ROW_UNMAPPER, 59 | new TableDescription("BOARDING_PASS", null, "flight_no", "seq_no")); 60 | } 61 | 62 | 63 | @Override 64 | protected S postInsert(S entity, Number generatedId) { 65 | entity.withPersisted(true); 66 | return entity; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/cz/jirutka/spring/data/jdbc/fixtures/UserRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.fixtures; 18 | 19 | import cz.jirutka.spring.data.jdbc.BaseJdbcRepository; 20 | import cz.jirutka.spring.data.jdbc.RowUnmapper; 21 | import org.springframework.jdbc.core.RowMapper; 22 | import org.springframework.stereotype.Repository; 23 | 24 | import java.sql.Date; 25 | import java.sql.ResultSet; 26 | import java.sql.SQLException; 27 | import java.util.LinkedHashMap; 28 | import java.util.Map; 29 | 30 | @Repository 31 | public class UserRepository extends BaseJdbcRepository { 32 | 33 | public static final RowMapper ROW_MAPPER = new RowMapper() { 34 | 35 | public User mapRow(ResultSet rs, int rowNum) throws SQLException { 36 | return new User( 37 | rs.getString("user_name"), 38 | rs.getDate("date_of_birth"), 39 | rs.getInt("reputation"), 40 | rs.getBoolean("enabled") 41 | ).withPersisted(true); 42 | } 43 | }; 44 | 45 | public static final RowUnmapper ROW_UNMAPPER = new RowUnmapper() { 46 | 47 | public Map mapColumns(User o) { 48 | LinkedHashMap row = new LinkedHashMap<>(); 49 | row.put("user_name", o.getUserName()); 50 | row.put("date_of_birth", new Date(o.getDateOfBirth().getTime())); 51 | row.put("reputation", o.getReputation()); 52 | row.put("enabled", o.isEnabled()); 53 | return row; 54 | } 55 | }; 56 | 57 | 58 | public UserRepository() { 59 | super(ROW_MAPPER, ROW_UNMAPPER, "USERS", "user_name"); 60 | } 61 | 62 | 63 | @Override 64 | protected S postUpdate(S entity) { 65 | entity.withPersisted(true); 66 | return entity; 67 | } 68 | 69 | @Override 70 | protected S postInsert(S entity, Number generatedId) { 71 | entity.withPersisted(true); 72 | return entity; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/cz/jirutka/spring/data/jdbc/fixtures/CommentRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.fixtures; 18 | 19 | import cz.jirutka.spring.data.jdbc.BaseJdbcRepository; 20 | import cz.jirutka.spring.data.jdbc.RowUnmapper; 21 | import cz.jirutka.spring.data.jdbc.TableDescription; 22 | import org.springframework.jdbc.core.RowMapper; 23 | import org.springframework.stereotype.Repository; 24 | 25 | import java.sql.ResultSet; 26 | import java.sql.SQLException; 27 | import java.util.LinkedHashMap; 28 | import java.util.Map; 29 | 30 | @Repository 31 | public class CommentRepository extends BaseJdbcRepository { 32 | 33 | public static final RowMapper ROW_MAPPER = new RowMapper() { 34 | 35 | public Comment mapRow(ResultSet rs, int rowNum) throws SQLException { 36 | return new Comment( 37 | rs.getInt("id"), 38 | rs.getString("user_name"), 39 | rs.getString("contents"), 40 | rs.getTimestamp("created_time"), 41 | rs.getInt("favourite_count") 42 | ); 43 | } 44 | }; 45 | 46 | public static final RowUnmapper ROW_UNMAPPER = new RowUnmapper() { 47 | 48 | public Map mapColumns(Comment o) { 49 | Map row = new LinkedHashMap<>(); 50 | row.put("id", o.getId()); 51 | row.put("user_name", o.getUserName()); 52 | row.put("contents", o.getContents()); 53 | row.put("created_time", new java.sql.Timestamp(o.getCreatedTime().getTime())); 54 | row.put("favourite_count", o.getFavouriteCount()); 55 | return row; 56 | } 57 | }; 58 | 59 | 60 | public CommentRepository() { 61 | super(ROW_MAPPER, ROW_UNMAPPER, "COMMENTS"); 62 | } 63 | 64 | public CommentRepository(TableDescription table) { 65 | super(ROW_MAPPER, ROW_UNMAPPER, table); 66 | } 67 | 68 | 69 | @Override 70 | protected S postInsert(S entity, Number generatedId) { 71 | entity.setId(generatedId.intValue()); 72 | return entity; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/MySqlJdbcRepositoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource 19 | import cz.jirutka.spring.data.jdbc.config.AbstractTestConfig 20 | import cz.jirutka.spring.data.jdbc.sql.LimitOffsetSqlGenerator 21 | import cz.jirutka.spring.data.jdbc.sql.SqlGeneratorFactoryIT 22 | import groovy.transform.AnnotationCollector 23 | import org.springframework.context.annotation.Bean 24 | import org.springframework.context.annotation.Configuration 25 | import org.springframework.test.context.ContextConfiguration 26 | import org.springframework.transaction.annotation.EnableTransactionManagement 27 | import spock.lang.Requires 28 | 29 | import javax.sql.DataSource 30 | 31 | import static MySqlTestConfig.MYSQL_HOST 32 | import static TestUtils.env 33 | import static TestUtils.isPortInUse 34 | 35 | @MySqlTestContext 36 | class MySqlJdbcRepositoryCompoundPkIT extends JdbcRepositoryCompoundPkIT {} 37 | 38 | @MySqlTestContext 39 | class MySqlJdbcRepositoryGeneratedKeyIT extends JdbcRepositoryGeneratedKeyIT {} 40 | 41 | @MySqlTestContext 42 | class MySqlJdbcRepositoryManualKeyIT extends JdbcRepositoryManualKeyIT {} 43 | 44 | @MySqlTestContext 45 | class MySqlJdbcRepositoryManyToOneIT extends JdbcRepositoryManyToOneIT {} 46 | 47 | @MySqlTestContext 48 | class MySqlSqlGeneratorFactoryIT extends SqlGeneratorFactoryIT { 49 | Class getExpectedGenerator() { LimitOffsetSqlGenerator } 50 | } 51 | 52 | @AnnotationCollector 53 | @Requires({ env('CI') ? env('DB').equals('mysql') : isPortInUse(MYSQL_HOST, 3306) }) 54 | @ContextConfiguration(classes = MySqlTestConfig) 55 | @interface MySqlTestContext {} 56 | 57 | @Configuration 58 | @EnableTransactionManagement 59 | class MySqlTestConfig extends AbstractTestConfig { 60 | 61 | static final String MYSQL_HOST = env('MYSQL_HOST', 'localhost') 62 | 63 | final initSqlScript = 'schema_mysql.sql' 64 | 65 | 66 | @Bean DataSource dataSource() { 67 | new MysqlConnectionPoolDataSource ( 68 | serverName: MYSQL_HOST, 69 | user: env('MYSQL_USER', 'root'), 70 | password: env('MYSQL_PASSWORD', ''), 71 | databaseName: DATABASE_NAME 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/config/AbstractTestConfig.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.config 18 | 19 | import cz.jirutka.spring.data.jdbc.fixtures.BoardingPassRepository 20 | import cz.jirutka.spring.data.jdbc.fixtures.CommentRepository 21 | import cz.jirutka.spring.data.jdbc.fixtures.CommentWithUserRepository 22 | import cz.jirutka.spring.data.jdbc.fixtures.UserRepository 23 | import org.springframework.beans.factory.annotation.Autowired 24 | import org.springframework.context.annotation.Bean 25 | import org.springframework.core.io.ClassPathResource 26 | import org.springframework.core.io.ResourceLoader 27 | import org.springframework.jdbc.datasource.DataSourceTransactionManager 28 | import org.springframework.jdbc.datasource.init.DataSourceInitializer 29 | import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator 30 | import org.springframework.transaction.PlatformTransactionManager 31 | 32 | import javax.sql.DataSource 33 | 34 | abstract class AbstractTestConfig { 35 | 36 | static final String DATABASE_NAME = 'spring_data_jdbc_repository_test' 37 | 38 | @Autowired ResourceLoader resourceLoader 39 | 40 | 41 | @Bean abstract DataSource dataSource() 42 | 43 | 44 | def getInitSqlScript() { 45 | } 46 | 47 | @Bean dataSourceInitializer() { 48 | if (!initSqlScript) { 49 | return null 50 | } 51 | new DataSourceInitializer ( 52 | dataSource: dataSource(), 53 | databasePopulator: new ResourceDatabasePopulator( 54 | scripts: new ClassPathResource(initSqlScript) 55 | ) 56 | ) 57 | } 58 | 59 | @Bean PlatformTransactionManager transactionManager() { 60 | new DataSourceTransactionManager( dataSource() ) 61 | } 62 | 63 | @Bean CommentRepository commentRepository() { 64 | new CommentRepository() 65 | } 66 | 67 | @Bean UserRepository userRepository() { 68 | new UserRepository() 69 | } 70 | 71 | @Bean BoardingPassRepository boardingPassRepository() { 72 | new BoardingPassRepository() 73 | } 74 | 75 | @Bean CommentWithUserRepository commentWithUserRepository() { 76 | new CommentWithUserRepository() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/OracleJdbcRepositoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import com.zaxxer.hikari.HikariDataSource 19 | import cz.jirutka.spring.data.jdbc.config.AbstractTestConfig 20 | import cz.jirutka.spring.data.jdbc.sql.Oracle9SqlGenerator 21 | import cz.jirutka.spring.data.jdbc.sql.SqlGeneratorFactoryIT 22 | import groovy.transform.AnnotationCollector 23 | import org.springframework.context.annotation.Bean 24 | import org.springframework.context.annotation.Configuration 25 | import org.springframework.test.context.ContextConfiguration 26 | import org.springframework.transaction.annotation.EnableTransactionManagement 27 | import spock.lang.Requires 28 | 29 | import javax.sql.DataSource 30 | 31 | import static OracleTestConfig.ORACLE_HOST 32 | import static TestUtils.env 33 | import static TestUtils.isPortInUse 34 | 35 | @OracleTestContext 36 | class OracleJdbcRepositoryCompoundPkIT extends JdbcRepositoryCompoundPkIT {} 37 | 38 | @OracleTestContext 39 | class OracleJdbcRepositoryGeneratedKeyIT extends JdbcRepositoryGeneratedKeyIT {} 40 | 41 | @OracleTestContext 42 | class OracleJdbcRepositoryManualKeyIT extends JdbcRepositoryManualKeyIT {} 43 | 44 | @OracleTestContext 45 | class OracleJdbcRepositoryManyToOneIT extends JdbcRepositoryManyToOneIT {} 46 | 47 | @OracleTestContext 48 | class OracleSqlGeneratorFactoryIT extends SqlGeneratorFactoryIT { 49 | Class getExpectedGenerator() { Oracle9SqlGenerator } 50 | } 51 | 52 | @AnnotationCollector 53 | @Requires({ env('CI') ? env('DB').equals('oracle') : isPortInUse(ORACLE_HOST, 1521) }) 54 | @ContextConfiguration(classes = OracleTestConfig) 55 | @interface OracleTestContext {} 56 | 57 | @Configuration 58 | @EnableTransactionManagement 59 | class OracleTestConfig extends AbstractTestConfig { 60 | 61 | static final String ORACLE_HOST = env('ORACLE_HOST', 'localhost') 62 | 63 | 64 | @Bean(destroyMethod = 'shutdown') 65 | def DataSource dataSource() { 66 | new HikariDataSource( 67 | dataSourceClassName: 'oracle.jdbc.pool.OracleDataSource', 68 | dataSourceProperties: [ 69 | driverType: 'thin', 70 | serverName: ORACLE_HOST, 71 | portNumber: 1521, 72 | serviceName: env('ORACLE_SID', 'XE'), 73 | user: env('ORACLE_USER', 'test'), 74 | password: env('ORACLE_PASSWORD', 'test') 75 | ] 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/MariaDbJdbcRepositoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import com.zaxxer.hikari.HikariDataSource 19 | import cz.jirutka.spring.data.jdbc.config.AbstractTestConfig 20 | import cz.jirutka.spring.data.jdbc.sql.LimitOffsetSqlGenerator 21 | import cz.jirutka.spring.data.jdbc.sql.SqlGeneratorFactoryIT 22 | import groovy.transform.AnnotationCollector 23 | import org.springframework.context.annotation.Bean 24 | import org.springframework.context.annotation.Configuration 25 | import org.springframework.test.context.ContextConfiguration 26 | import org.springframework.transaction.annotation.EnableTransactionManagement 27 | import spock.lang.Requires 28 | 29 | import javax.sql.DataSource 30 | 31 | import static MariaDbTestConfig.MARIADB_HOST 32 | import static TestUtils.env 33 | import static TestUtils.isPortInUse 34 | 35 | @MariaDbTestContext 36 | class MariaDbJdbcRepositoryCompoundPkIT extends JdbcRepositoryCompoundPkIT {} 37 | 38 | @MariaDbTestContext 39 | class MariaDbJdbcRepositoryGeneratedKeyIT extends JdbcRepositoryGeneratedKeyIT {} 40 | 41 | @MariaDbTestContext 42 | class MariaDbJdbcRepositoryManualKeyIT extends JdbcRepositoryManualKeyIT {} 43 | 44 | @MariaDbTestContext 45 | class MariaDbJdbcRepositoryManyToOneIT extends JdbcRepositoryManyToOneIT {} 46 | 47 | @MariaDbTestContext 48 | class MariaDbSqlGeneratorFactoryIT extends SqlGeneratorFactoryIT { 49 | Class getExpectedGenerator() { LimitOffsetSqlGenerator } 50 | } 51 | 52 | @AnnotationCollector 53 | @Requires({ env('CI') ? env('DB').equals('mariadb') : isPortInUse(MARIADB_HOST, 3306) }) 54 | @ContextConfiguration(classes = MariaDbTestConfig) 55 | @interface MariaDbTestContext {} 56 | 57 | @Configuration 58 | @EnableTransactionManagement 59 | class MariaDbTestConfig extends AbstractTestConfig { 60 | 61 | static final String MARIADB_HOST = env('MARIADB_HOST', 'localhost') 62 | 63 | final initSqlScript = 'schema_mysql.sql' 64 | 65 | 66 | @Bean(destroyMethod = 'shutdown') 67 | def DataSource dataSource() { 68 | new HikariDataSource( 69 | dataSourceClassName: 'org.mariadb.jdbc.MariaDbDataSource', 70 | dataSourceProperties: [ 71 | serverName: MARIADB_HOST, 72 | portNumber: 3306, 73 | user: env('MARIADB_USER', 'root'), 74 | password: env('MARIADB_PASSWORD', ''), 75 | databaseName: DATABASE_NAME, 76 | ] 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/JdbcRepositoryGeneratedKeyIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc 18 | 19 | import cz.jirutka.spring.data.jdbc.fixtures.Comment 20 | import cz.jirutka.spring.data.jdbc.fixtures.CommentRepository 21 | import cz.jirutka.spring.data.jdbc.fixtures.User 22 | import cz.jirutka.spring.data.jdbc.fixtures.UserRepository 23 | import org.springframework.transaction.annotation.Transactional 24 | import spock.lang.Specification 25 | import spock.lang.Unroll 26 | 27 | import javax.annotation.Resource 28 | 29 | @Unroll 30 | @Transactional 31 | abstract class JdbcRepositoryGeneratedKeyIT extends Specification { 32 | 33 | @Resource CommentRepository repository 34 | @Resource UserRepository userRepository 35 | 36 | final someUser = 'some_user' 37 | 38 | 39 | def setup() { 40 | userRepository.save(new User(someUser, new Date(), -1, false)) 41 | } 42 | 43 | 44 | def '#method(T): generates key'() { 45 | given: 46 | def comment = createComment() 47 | when: 48 | repository./$method/(comment) 49 | then: 50 | comment.id != null 51 | where: 52 | method << ['save', 'insert'] 53 | } 54 | 55 | def '#method(T): generates subsequent ids'() { 56 | given: 57 | def first = createComment() 58 | def second = createComment() 59 | when: 60 | repository./$method/(first) 61 | repository./$method/(second) 62 | then: 63 | first.id < second.id 64 | where: 65 | method << ['save', 'insert'] 66 | } 67 | 68 | def '#method(T): updates the record when already exists'() { 69 | given: 70 | def oldDate = new Date(100000000) 71 | def newDate = new Date(200000000) 72 | and: 73 | def comment = repository.save(new Comment(null, someUser, 'Some content', oldDate, 0)) 74 | def modifiedComment = new Comment(comment.id, someUser, 'New content', newDate, 1) 75 | when: 76 | def updatedComment = repository./$method/(modifiedComment) 77 | then: 78 | repository.count() == 1 79 | updatedComment == modifiedComment 80 | where: 81 | method << ['save', 'update'] 82 | } 83 | 84 | 85 | private createComment() { 86 | new Comment(null, someUser, 'Some content', new Date(), 0) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/cz/jirutka/spring/data/jdbc/fixtures/CommentWithUserRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.fixtures; 18 | 19 | import cz.jirutka.spring.data.jdbc.BaseJdbcRepository; 20 | import cz.jirutka.spring.data.jdbc.RowUnmapper; 21 | import cz.jirutka.spring.data.jdbc.TableDescription; 22 | import org.springframework.jdbc.core.RowMapper; 23 | import org.springframework.stereotype.Repository; 24 | 25 | import java.sql.ResultSet; 26 | import java.sql.SQLException; 27 | import java.sql.Timestamp; 28 | import java.util.LinkedHashMap; 29 | import java.util.Map; 30 | 31 | @Repository 32 | public class CommentWithUserRepository extends BaseJdbcRepository { 33 | 34 | public static final RowMapper ROW_MAPPER = new RowMapper() { 35 | 36 | public CommentWithUser mapRow(ResultSet rs, int rowNum) throws SQLException { 37 | User user = UserRepository.ROW_MAPPER.mapRow(rs, rowNum); 38 | return new CommentWithUser( 39 | rs.getInt("id"), 40 | user, 41 | rs.getString("contents"), 42 | rs.getTimestamp("created_time"), 43 | rs.getInt("favourite_count") 44 | ); 45 | } 46 | }; 47 | 48 | public static final RowUnmapper ROW_UNMAPPER = new RowUnmapper() { 49 | 50 | public Map mapColumns(CommentWithUser o) { 51 | Map cols = new LinkedHashMap<>(); 52 | cols.put("id", o.getId()); 53 | cols.put("user_name", o.getUser().getUserName()); 54 | cols.put("contents", o.getContents()); 55 | cols.put("created_time", new Timestamp(o.getCreatedTime().getTime())); 56 | cols.put("favourite_count", o.getFavouriteCount()); 57 | return cols; 58 | } 59 | }; 60 | 61 | 62 | public CommentWithUserRepository() { 63 | this(new TableDescription( 64 | "COMMENTS", "COMMENTS JOIN USERS ON COMMENTS.user_name = USERS.user_name", "id")); 65 | } 66 | 67 | public CommentWithUserRepository(TableDescription table) { 68 | super(ROW_MAPPER, ROW_UNMAPPER, table); 69 | } 70 | 71 | 72 | @Override 73 | protected S postInsert(S entity, Number generatedId) { 74 | entity.setId(generatedId.intValue()); 75 | return entity; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/PostgresqlJdbcRepositoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import com.zaxxer.hikari.HikariDataSource 19 | import cz.jirutka.spring.data.jdbc.config.AbstractTestConfig 20 | import cz.jirutka.spring.data.jdbc.sql.LimitOffsetSqlGenerator 21 | import cz.jirutka.spring.data.jdbc.sql.SqlGeneratorFactoryIT 22 | import groovy.transform.AnnotationCollector 23 | import org.springframework.context.annotation.Bean 24 | import org.springframework.context.annotation.Configuration 25 | import org.springframework.test.context.ContextConfiguration 26 | import org.springframework.transaction.annotation.EnableTransactionManagement 27 | import spock.lang.Requires 28 | 29 | import javax.sql.DataSource 30 | 31 | import static PostgresqlTestConfig.POSTGRESQL_HOST 32 | import static TestUtils.env 33 | import static TestUtils.isPortInUse 34 | 35 | @PostgresqlTestContext 36 | class PostgresqlJdbcRepositoryCompoundPkIT extends JdbcRepositoryCompoundPkIT {} 37 | 38 | @PostgresqlTestContext 39 | class PostgresqlJdbcRepositoryGeneratedKeyIT extends JdbcRepositoryGeneratedKeyIT {} 40 | 41 | @PostgresqlTestContext 42 | class PostgresqlJdbcRepositoryManualKeyIT extends JdbcRepositoryManualKeyIT {} 43 | 44 | @PostgresqlTestContext 45 | class PostgresqlJdbcRepositoryManyToOneIT extends JdbcRepositoryManyToOneIT {} 46 | 47 | @PostgresqlTestContext 48 | class PostgresqlSqlGeneratorFactoryIT extends SqlGeneratorFactoryIT { 49 | Class getExpectedGenerator() { LimitOffsetSqlGenerator } 50 | } 51 | 52 | @AnnotationCollector 53 | @Requires({ env('CI') ? env('DB').equals('postgresql') : isPortInUse(POSTGRESQL_HOST, 5432) }) 54 | @ContextConfiguration(classes = PostgresqlTestConfig) 55 | @interface PostgresqlTestContext {} 56 | 57 | @Configuration 58 | @EnableTransactionManagement 59 | class PostgresqlTestConfig extends AbstractTestConfig { 60 | 61 | static final String POSTGRESQL_HOST = env('POSTGRESQL_HOST', 'localhost') 62 | 63 | final initSqlScript = 'schema_postgresql.sql' 64 | 65 | 66 | @Bean(destroyMethod = 'shutdown') 67 | def DataSource dataSource() { 68 | new HikariDataSource( 69 | dataSourceClassName: 'org.postgresql.ds.PGSimpleDataSource', 70 | dataSourceProperties: [ 71 | serverName: POSTGRESQL_HOST, 72 | portNumber: 5432, 73 | user: env('POSTGRESQL_USER', 'postgres'), 74 | password: env('POSTGRESQL_PASSWORD', ''), 75 | databaseName: DATABASE_NAME, 76 | ] 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/DerbyJdbcRepositoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import cz.jirutka.spring.data.jdbc.config.AbstractTestConfig 19 | import cz.jirutka.spring.data.jdbc.fixtures.CommentRepository 20 | import cz.jirutka.spring.data.jdbc.fixtures.CommentWithUserRepository 21 | import cz.jirutka.spring.data.jdbc.sql.SQL2008SqlGenerator 22 | import cz.jirutka.spring.data.jdbc.sql.SqlGeneratorFactoryIT 23 | import groovy.transform.AnnotationCollector 24 | import org.springframework.context.annotation.Bean 25 | import org.springframework.context.annotation.Configuration 26 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder 27 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType 28 | import org.springframework.test.context.ContextConfiguration 29 | import org.springframework.transaction.annotation.EnableTransactionManagement 30 | import spock.lang.Requires 31 | 32 | import javax.sql.DataSource 33 | 34 | import static TestUtils.env 35 | 36 | @DerbyTestContext 37 | class DerbyJdbcRepositoryCompoundPkIT extends JdbcRepositoryCompoundPkIT {} 38 | 39 | @DerbyTestContext 40 | class DerbyJdbcRepositoryGeneratedKeyIT extends JdbcRepositoryGeneratedKeyIT {} 41 | 42 | @DerbyTestContext 43 | class DerbyJdbcRepositoryManualKeyIT extends JdbcRepositoryManualKeyIT {} 44 | 45 | @DerbyTestContext 46 | class DerbyJdbcRepositoryManyToOneIT extends JdbcRepositoryManyToOneIT {} 47 | 48 | @DerbyTestContext 49 | class DerbySqlGeneratorFactoryIT extends SqlGeneratorFactoryIT { 50 | Class getExpectedGenerator() { SQL2008SqlGenerator } 51 | } 52 | 53 | @AnnotationCollector 54 | @Requires({ env('CI') ? env('DB').equals('embedded') : true }) 55 | @ContextConfiguration(classes = DerbyTestConfig) 56 | @interface DerbyTestContext {} 57 | 58 | @Configuration 59 | @EnableTransactionManagement 60 | class DerbyTestConfig extends AbstractTestConfig { 61 | 62 | @Bean CommentRepository commentRepository() { 63 | new CommentRepository(new TableDescription('COMMENTS', 'ID')) 64 | } 65 | 66 | @Bean CommentWithUserRepository commentWithUserRepository() { 67 | new CommentWithUserRepository(new TableDescription( 68 | tableName: 'COMMENTS', 69 | fromClause: 'COMMENTS JOIN USERS ON COMMENTS.user_name = USERS.user_name', 70 | pkColumns: ['ID'] 71 | )) 72 | } 73 | 74 | @Bean DataSource dataSource() { 75 | new EmbeddedDatabaseBuilder() 76 | .addScript('schema_derby.sql') 77 | .setType(EmbeddedDatabaseType.DERBY) 78 | .build() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/Mssql2012JdbcRepositoryIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the 'License'); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an 'AS IS' BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc 17 | 18 | import com.zaxxer.hikari.HikariDataSource 19 | import cz.jirutka.spring.data.jdbc.config.AbstractTestConfig 20 | import cz.jirutka.spring.data.jdbc.sql.SQL2008SqlGenerator 21 | import cz.jirutka.spring.data.jdbc.sql.SqlGeneratorFactoryIT 22 | import groovy.transform.AnnotationCollector 23 | import org.springframework.context.annotation.Bean 24 | import org.springframework.context.annotation.Configuration 25 | import org.springframework.test.context.ContextConfiguration 26 | import org.springframework.transaction.annotation.EnableTransactionManagement 27 | import spock.lang.Requires 28 | 29 | import javax.sql.DataSource 30 | 31 | import static Mssql2012TestConfig.MSSQL_HOST 32 | import static TestUtils.env 33 | import static TestUtils.isPortInUse 34 | 35 | @Mssql2012TestContext 36 | class Mssql2012JdbcRepositoryCompoundPkIT extends JdbcRepositoryCompoundPkIT {} 37 | 38 | @Mssql2012TestContext 39 | class Mssql2012JdbcRepositoryGeneratedKeyIT extends JdbcRepositoryGeneratedKeyIT {} 40 | 41 | @Mssql2012TestContext 42 | class Mssql2012JdbcRepositoryManualKeyIT extends JdbcRepositoryManualKeyIT {} 43 | 44 | @Mssql2012TestContext 45 | class Mssql2012JdbcRepositoryManyToOneIT extends JdbcRepositoryManyToOneIT {} 46 | 47 | @Mssql2012TestContext 48 | class Mssql2012SqlGeneratorFactoryIT extends SqlGeneratorFactoryIT { 49 | Class getExpectedGenerator() { SQL2008SqlGenerator } 50 | } 51 | 52 | @AnnotationCollector 53 | @Requires({ env('CI') ? env('DB').equals('mssql') : isPortInUse(MSSQL_HOST, 1433) }) 54 | @ContextConfiguration(classes = Mssql2012TestConfig) 55 | @interface Mssql2012TestContext {} 56 | 57 | @Configuration 58 | @EnableTransactionManagement 59 | class Mssql2012TestConfig extends AbstractTestConfig { 60 | 61 | static final String MSSQL_HOST = env('MSSQL_HOST', 'localhost') 62 | 63 | final initSqlScript = 'schema_mssql.sql' 64 | 65 | 66 | @Bean(destroyMethod = 'shutdown') 67 | DataSource dataSource() { 68 | new HikariDataSource( 69 | dataSourceClassName: 'net.sourceforge.jtds.jdbcx.JtdsDataSource', 70 | connectionTestQuery: 'select 1', 71 | dataSourceProperties: [ 72 | serverName: MSSQL_HOST, 73 | instance: env('MSSQL_INSTANCE', 'SQL2012SP1'), 74 | user: env('MSSQL_USER', 'sa'), 75 | password: env('MSSQL_PASSWORD', 'Password12!'), 76 | databaseName: AbstractTestConfig.DATABASE_NAME 77 | ] 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/JdbcRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc; 17 | 18 | import org.springframework.data.domain.Sort; 19 | import org.springframework.data.repository.NoRepositoryBean; 20 | import org.springframework.data.repository.PagingAndSortingRepository; 21 | 22 | import java.io.Serializable; 23 | import java.util.List; 24 | 25 | /** 26 | * JDBC specific extension of {@link org.springframework.data.repository.Repository}. 27 | * 28 | * @param the domain type the repository manages. 29 | * @param the type of the id of the entity the repository manages. 30 | */ 31 | @NoRepositoryBean 32 | public interface JdbcRepository extends PagingAndSortingRepository { 33 | 34 | List findAll(); 35 | 36 | List findAll(Sort sort); 37 | 38 | List findAll(Iterable ids); 39 | 40 | /** 41 | * Saves the given entity. If {@link org.springframework.data.domain.Persistable#isNew() 42 | * entity.isNew()} returns true, then it creates a new record; otherwise it 43 | * updates the existing one. 44 | * 45 | *

Use the returned instance for further operations as the save 46 | * operation might have changed the entity instance completely.

47 | * 48 | * @param entity 49 | * @return A saved entity. 50 | * @throws IllegalArgumentException if the given entity is null. 51 | */ 52 | S save(S entity); 53 | 54 | /** 55 | * Saves all the given entities. 56 | * 57 | * @see #save(S) 58 | * @param entities 59 | * @return Saved entities. 60 | * @throws IllegalArgumentException if one of the given entities is null. 61 | */ 62 | List save(Iterable entities); 63 | 64 | /** 65 | * Inserts the given new entity into database. 66 | * 67 | *

Use the returned instance for further operations as the insert 68 | * operation might have changed the entity instance.

69 | * 70 | * @param entity 71 | * @return An inserted entity. 72 | * @throws org.springframework.dao.DuplicateKeyException if record with the 73 | * same primary key as the given entity already exists. 74 | */ 75 | S insert(S entity); 76 | 77 | /** 78 | * Updates the given entity. If no record with the entity's ID exists in 79 | * the database, then it throws an exception. 80 | * 81 | *

Use the returned instance for further operations as the update 82 | * operation might have changed the entity instance.

83 | * 84 | * @param entity 85 | * @return An updated entity. 86 | * @throws NoRecordUpdatedException if the entity doesn't exist (i.e. no 87 | * record has been updated). 88 | * @throws IllegalArgumentException if some of the properties mapped to the 89 | * entity's primary key are null. 90 | */ 91 | S update(S entity); 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/TableDescription.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc; 18 | 19 | import org.springframework.util.Assert; 20 | 21 | import java.util.List; 22 | 23 | import static java.util.Arrays.asList; 24 | import static java.util.Collections.singletonList; 25 | import static java.util.Collections.unmodifiableList; 26 | 27 | public class TableDescription { 28 | 29 | private String tableName; 30 | private String selectClause = "*"; 31 | private String fromClause; 32 | private List pkColumns = singletonList("id"); 33 | 34 | 35 | public TableDescription() { 36 | } 37 | 38 | public TableDescription(String tableName, String selectClause, String fromClause, List pkColumns) { 39 | setTableName(tableName); 40 | setSelectClause(selectClause); 41 | setFromClause(fromClause); 42 | setPkColumns(pkColumns); 43 | } 44 | 45 | public TableDescription(String tableName, String fromClause, String... pkColumns) { 46 | this(tableName, null, fromClause, asList(pkColumns)); 47 | } 48 | 49 | public TableDescription(String tableName, String idColumn) { 50 | this(tableName, null, idColumn); 51 | } 52 | 53 | 54 | /** 55 | * @see #setTableName(String) 56 | * @throws IllegalStateException if {@code tableName} is not set. 57 | */ 58 | public String getTableName() { 59 | Assert.state(tableName != null, "tableName must not be null"); 60 | return tableName; 61 | } 62 | 63 | /** 64 | * @param tableName The table name. 65 | * @throws IllegalArgumentException if {@code tableName} is blank. 66 | */ 67 | public void setTableName(String tableName) { 68 | Assert.hasText(tableName, "tableName must not be blank"); 69 | this.tableName = tableName; 70 | } 71 | 72 | /** 73 | * @see #setSelectClause(String) 74 | */ 75 | public String getSelectClause() { 76 | return selectClause; 77 | } 78 | 79 | /** 80 | * @param selectClause The expression to be used in SELECT clause, i.e. 81 | * list of columns to be retrieved. Default is {@code *}. 82 | */ 83 | public void setSelectClause(String selectClause) { 84 | this.selectClause = selectClause != null ? selectClause : "*"; 85 | } 86 | 87 | /** 88 | * @see #setSelectClause(String) 89 | */ 90 | public String getFromClause() { 91 | return fromClause != null ? fromClause : getTableName(); 92 | } 93 | 94 | /** 95 | * @param fromClause The expression to be used in SELECT ... FROM clause, 96 | * i.e. table and join clauses. Defaults to {@link #getTableName()}. 97 | */ 98 | public void setFromClause(String fromClause) { 99 | this.fromClause = fromClause; 100 | } 101 | 102 | /** 103 | * @see #setFromClause(String) 104 | */ 105 | public List getPkColumns() { 106 | return pkColumns; 107 | } 108 | 109 | /** 110 | * @param pkColumns A list of columns names that are part of the table's 111 | * primary key. 112 | * @throws IllegalArgumentException if {@code pkColumn} is empty. 113 | */ 114 | public void setPkColumns(List pkColumns) { 115 | Assert.notEmpty(pkColumns, "At least one primary key column must be provided"); 116 | this.pkColumns = unmodifiableList(pkColumns); 117 | } 118 | 119 | /** 120 | * @see #setPkColumns(List) 121 | */ 122 | public void setPkColumns(String... idColumns) { 123 | setPkColumns(asList(idColumns)); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/sql/SqlGeneratorFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.sql; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.dao.DataAccessResourceFailureException; 21 | 22 | import javax.sql.DataSource; 23 | import java.sql.DatabaseMetaData; 24 | import java.sql.SQLException; 25 | import java.util.ArrayDeque; 26 | import java.util.Deque; 27 | import java.util.Map; 28 | import java.util.WeakHashMap; 29 | 30 | public class SqlGeneratorFactory { 31 | 32 | private static final Logger LOG = LoggerFactory.getLogger(SqlGeneratorFactory.class); 33 | 34 | private static final SqlGeneratorFactory INSTANCE = new SqlGeneratorFactory(true); 35 | 36 | private final Deque generators = new ArrayDeque<>(); 37 | 38 | private final Map cache = new WeakHashMap<>(2, 1.0f); 39 | 40 | 41 | /** 42 | * @param registerDefault Whether to register default (built-in) generators. 43 | * @see #getInstance() 44 | */ 45 | public SqlGeneratorFactory(boolean registerDefault) { 46 | if (registerDefault) { 47 | registerGenerator(new DefaultSqlGenerator()); 48 | registerGenerator(new LimitOffsetSqlGenerator()); 49 | registerGenerator(new SQL2008SqlGenerator()); 50 | registerGenerator(new Oracle9SqlGenerator()); 51 | } 52 | } 53 | 54 | /** 55 | * @return The singleton instance of SqlGeneratorFactory. 56 | */ 57 | public static SqlGeneratorFactory getInstance() { 58 | return INSTANCE; 59 | } 60 | 61 | 62 | /** 63 | * @param dataSource The DataSource for which to find compatible 64 | * SQL Generator. 65 | * @return An SQL Generator compatible with the given {@code dataSource}. 66 | * @throws DataAccessResourceFailureException if exception is thrown when 67 | * trying to obtain Connection or MetaData from the 68 | * {@code dataSource}. 69 | * @throws IllegalStateException if no compatible SQL Generator is found. 70 | */ 71 | public SqlGenerator getGenerator(DataSource dataSource) { 72 | 73 | if (cache.containsKey(dataSource)) { 74 | return cache.get(dataSource); 75 | } 76 | 77 | DatabaseMetaData metaData; 78 | try { 79 | metaData = dataSource.getConnection().getMetaData(); 80 | } catch (SQLException ex) { 81 | throw new DataAccessResourceFailureException( 82 | "Failed to retrieve database metadata", ex); 83 | } 84 | 85 | for (SqlGenerator generator : generators) { 86 | try { 87 | if (generator.isCompatible(metaData)) { 88 | LOG.info("Using SQL Generator {} for dataSource {}", 89 | generator.getClass().getName(), dataSource.getClass()); 90 | 91 | cache.put(dataSource, generator); 92 | return generator; 93 | } 94 | } catch (SQLException ex) { 95 | LOG.warn("Exception occurred when invoking isCompatible() on {}", 96 | generator.getClass().getSimpleName(), ex); 97 | } 98 | } 99 | 100 | // This should not happen, because registry should always contain one 101 | // "default" generator that returns true for every DatabaseMetaData. 102 | throw new IllegalStateException("No compatible SQL Generator found."); 103 | } 104 | 105 | /** 106 | * Adds the {@code sqlGenerator} to the top of the generators registry. 107 | * 108 | * @param sqlGenerator The SQL Generator instance to register. 109 | */ 110 | public void registerGenerator(SqlGenerator sqlGenerator) { 111 | generators.push(sqlGenerator); 112 | } 113 | 114 | /** 115 | * Removes all generators from the factory's registry. 116 | */ 117 | public void clear() { 118 | generators.clear(); 119 | cache.clear(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/sql/SqlGeneratorFactoryTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.data.jdbc.sql 17 | 18 | import org.springframework.dao.DataAccessResourceFailureException 19 | import spock.lang.Specification 20 | 21 | import javax.sql.DataSource 22 | import java.sql.Connection 23 | import java.sql.DatabaseMetaData 24 | import java.sql.SQLException 25 | 26 | class SqlGeneratorFactoryTest extends Specification { 27 | 28 | def factory = new SqlGeneratorFactory(true) 29 | 30 | def sqlGenerator = Mock(SqlGenerator) 31 | def dbMetaData = Stub(DatabaseMetaData) 32 | 33 | def dataSource = Mock(DataSource) { 34 | getConnection() >> Mock(Connection) { 35 | getMetaData() >> dbMetaData 36 | } 37 | } 38 | 39 | 40 | def 'getInstance(): returns singleton instance with registered generators'() { 41 | expect: 42 | SqlGeneratorFactory.getInstance() != null 43 | SqlGeneratorFactory.getInstance().is(SqlGeneratorFactory.getInstance()) 44 | ! SqlGeneratorFactory.getInstance().@generators.isEmpty() 45 | } 46 | 47 | 48 | def 'getGenerator(): returns first generator that responds with true for isCompatible()'() { 49 | setup: 50 | def sqlGenerator2 = Mock(SqlGenerator) 51 | def sqlGenerator3 = Mock(SqlGenerator) 52 | and: 53 | [sqlGenerator3, sqlGenerator2, sqlGenerator].each { 54 | factory.registerGenerator(it) 55 | } 56 | when: 57 | def actual = factory.getGenerator(dataSource) 58 | then: 59 | 1 * sqlGenerator.isCompatible(dbMetaData) >> false 60 | 1 * sqlGenerator2.isCompatible(dbMetaData) >> { throw new SQLException('Not me!') } 61 | 1 * sqlGenerator3.isCompatible(dbMetaData) >> true 62 | actual == sqlGenerator3 63 | } 64 | 65 | def 'getGenerator(): caches result'() { 66 | setup: 67 | def dbMetaData2 = Mock(DatabaseMetaData) 68 | def dataSource2 = Mock(DataSource) { 69 | getConnection() >> Mock(Connection) { 70 | getMetaData() >> dbMetaData2 71 | } 72 | } 73 | def sqlGenerator2 = Mock(SqlGenerator) 74 | and: 75 | factory.registerGenerator(sqlGenerator2) 76 | factory.registerGenerator(sqlGenerator) 77 | when: 78 | 2.times { assert factory.getGenerator(dataSource).is(sqlGenerator) } 79 | then: 80 | 1 * sqlGenerator.isCompatible(dbMetaData) >> true 81 | when: 82 | factory.getGenerator(dataSource2).is(sqlGenerator2) 83 | then: 84 | 1 * sqlGenerator.isCompatible(dbMetaData2) >> false 85 | 1 * sqlGenerator2.isCompatible(dbMetaData2) >> true 86 | when: 87 | factory.getGenerator(dataSource).is(sqlGenerator) 88 | then: 89 | 0 * sqlGenerator.isCompatible(_) 90 | } 91 | 92 | def 'getGenerator(): throws IllegalStateException when no compatible generator is found'() { 93 | setup: 94 | factory.clear() 95 | factory.registerGenerator(sqlGenerator) 96 | sqlGenerator.isCompatible(dbMetaData) >> false 97 | when: 98 | factory.getGenerator(dataSource) 99 | then: 100 | thrown IllegalStateException 101 | } 102 | 103 | def 'getGenerator(): throws DataAccessResourceFailureException when failed to get MetaData'() { 104 | when: 105 | factory.getGenerator(dataSource) 106 | then: 107 | 1 * dataSource.getConnection() >> { throw new SQLException('Oh crap!') } 108 | and: 109 | thrown DataAccessResourceFailureException 110 | } 111 | 112 | 113 | def 'registerGenerator(): adds given generator to the top of the generators stack'() { 114 | when: 115 | factory.registerGenerator(sqlGenerator) 116 | then: 117 | factory.@generators.first.is(sqlGenerator) 118 | } 119 | 120 | 121 | def 'clear(): removes all registered generators'() { 122 | setup: 123 | assert !factory.@generators.isEmpty() 124 | when: 125 | factory.clear() 126 | then: 127 | factory.@generators.isEmpty() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | = Changelog 2 | :issues-nurkiewicz-uri: https://github.com/nurkiewicz/spring-data-jdbc-repository/issues 3 | :issues-uri: https://github.com/jirutka/spring-data-jdbc-repository/issues 4 | 5 | 6 | == 0.6.0 (unreleased) 7 | 8 | Enhancements:: 9 | * Remove requirement for entities to implement `Persistable` interface. Entities may implement `Persistable`, or use annotation `@Id` from Spring Data Commons. 10 | * Add `SqlGeneratorFactory` that automatically selects compatible `SqlGenerator` according to used database. 11 | * Add protected getter for `jdbcOperations` ({issues-uri}/2[#2]). 12 | * Optimize `delete(Iterable)` – delete entities in a single query. 13 | * Make `update(S)` more robust – throw exception when ID is not null or wrong number of rows is (not) updated. 14 | 15 | Bug fixes:: 16 | * Fix duplication in test dependencies – exclude `groovy-all` from `spock-spring`. 17 | 18 | Changes:: 19 | * Rename base package from `com.nurkiewicz.jdbcrepository` to `cz.jirutka.spring.data.jdbc`. 20 | * `JdbcRepository`: 21 | ** remove method `pk(Object...)`, 22 | ** rename `JdbcRepository` to `BaseJdbcRepository` and create interface `JdbcRepository`, 23 | ** remove argument `sqlGenerator` from constructors and remove constructors without `rowUnmapper`, 24 | ** rename `getTable()` to `getTableDesc()`, 25 | ** rename `create(..)` to `insert(..)`, `preCreate(..)` to `preInsert(..)` and `postCreate` to `postInsert(..)`, 26 | ** remove overloaded method `postUpdate(S, int)` (it’s not needed anymore, see above), 27 | ** inject dependencies using `@Autowired` instead of manual lookup in `BeanFactory`, 28 | ** add required dependency on `DataSource` and make dependency on `JdbcOperations` optional, 29 | ** disallow properties change after initialization (invoking `afterPropertiesSet()`). 30 | * `TableDescription`: 31 | ** rename `getName()` to `getTableName()` and `getIdColumns()` to `getPkColumns()`, 32 | ** add property `selectClause`, 33 | ** add setters for all properties. 34 | * `SqlGenerator` and subclasses: 35 | ** remove field `allColumnsClause` and one-argument constructor (this is replaced by property `selectClause` in `TableDescription`), 36 | ** rename `SqlGenerator` to `DefaultSqlGenerator`, create interface `SqlGenerator` and reorganize subclasses. 37 | * Rename `MissingRowUnmapper` to `UnsupportedRowUnmapper`. 38 | 39 | Infrastructure:: 40 | * Update dependencies. 41 | * Refactor and extend tests for `SqlGenerator`. 42 | * Add tests for methods `insert(..)` and `update(..)`. 43 | 44 | 45 | == 0.5.0 (2016-02-15) 46 | 47 | ⭐️ Project forked and published under new coordinates: cz.jirutka.spring:spring-data-jdbc-repository. 48 | 49 | Enhancements:: 50 | * Rewrite all tests to Spock/Groovy. 51 | * Add overloaded hook method `JdbcRepository#postUpdate(Persistable, int)` with number of affected rows. 52 | * Improve `exists(ID)` performance; use `select 1` instead of `select count(*)`. 53 | * Change visibility of methods `JdbcRepository#update(Persistable)` and `JdbcRepository#create(Persistable)` to public. 54 | 55 | Bug fixes:: 56 | * Treat column names as case-insensitive ({issues-nurkiewicz-uri}/16[nurkiewicz#16]). 57 | * Fix autowiring of SqlGenerator bean ({issues-nurkiewicz-uri}/25[nurkiewicz#25]). 58 | 59 | Deprecations/changes:: 60 | * Drop support for Java 6, minimal required version is now 7. 61 | * Deprecate method `JdbcRepository.pk(Object...)` 62 | 63 | Infrastructure:: 64 | * Reformat and slightly refactor sources. 65 | * Test on CI with both OpenJDK 7 and OracleJDK 8. 66 | * Run integration tests for Oracle on Travis using Oracle XE installed with https://github.com/cbandy/travis-oracle[cbandy/travis-oracle]. 67 | * Run integration tests for MS SQL on AppVeyor using SQL Server 2012SP1 and 2014. 68 | * Add integration tests for MariaDB and run them on Travis. 69 | * Separate CI build jobs for embedded databases, PostgreSQL, MariaDB, MySQL, Oracle, and MSSQL. 70 | * Replace BoneCP with HikariCP in tests. 71 | * Inherit versions from Spring’s platform-bom. 72 | 73 | 74 | == 0.4.1 (2014-10-23) 75 | 76 | * Fixed standalone configuration and CDI Implementation ({issues-nurkiewicz-uri}/10[nurkiewicz#10]) 77 | 78 | == 0.4 (2014-06-16) 79 | 80 | * Repackaged: `com.blogspot.nurkiewicz` -> `com.nurkiewicz` 81 | 82 | == 0.3.2 (2014-06-16) 83 | 84 | * First version available in Maven central repository 85 | * Upgraded Spring Data Commons 1.6.1 -> 1.8.0 86 | 87 | == 0.3.1 (2013-03-16) 88 | 89 | * Upgraded Spring dependencies: 3.2.1 -> 3.2.4 and 1.5.0 -> 1.6.1 90 | * Allow manually injecting JdbcOperations, SqlGenerator and DataSource ({issues-nurkiewicz-uri}/5[nurkiewicz#5]) 91 | 92 | == 0.3 (2013-03-06) 93 | 94 | * Oracle 10g / 11g support ({issues-nurkiewicz-uri}/3[nurkiewicz#3]) 95 | * Upgrading Spring dependency to 3.2.1.RELEASE and http://www.springsource.org/spring-data/commons[Spring Data Commons] to 1.5.0.RELEASE ({issues-nurkiewicz-uri}/4[nurkiewicz#4]). 96 | 97 | == 0.2 (2013-01-23) 98 | 99 | * MS SQL Server 2008/2012 support ({issues-nurkiewicz-uri}/2[nurkiewicz#2]) 100 | 101 | == 0.1 (2013-01-20) 102 | 103 | * Initial revision (http://nurkiewicz.blogspot.no/2013/01/spring-data-jdbc-generic-dao.html[announcement]) 104 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/JdbcRepositoryCompoundPkIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License') 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc 18 | 19 | import cz.jirutka.spring.data.jdbc.fixtures.BoardingPass 20 | import cz.jirutka.spring.data.jdbc.fixtures.BoardingPassRepository 21 | import org.springframework.data.domain.PageRequest 22 | import org.springframework.data.domain.Sort 23 | import org.springframework.data.domain.Sort.Order 24 | import org.springframework.transaction.annotation.Transactional 25 | import spock.lang.Specification 26 | import spock.lang.Unroll 27 | 28 | import javax.annotation.Resource 29 | 30 | import static org.springframework.data.domain.Sort.Direction.ASC 31 | import static org.springframework.data.domain.Sort.Direction.DESC 32 | 33 | @Unroll 34 | @Transactional 35 | abstract class JdbcRepositoryCompoundPkIT extends Specification { 36 | 37 | @Resource BoardingPassRepository repository 38 | 39 | final entities = [ 40 | new BoardingPass('FOO-100', 1, 'Smith', 'B01'), 41 | new BoardingPass('FOO-100', 2, 'Johnson', 'C02'), 42 | new BoardingPass('BAR-100', 1, 'Gordon', 'D03'), 43 | new BoardingPass('BAR-100', 2, 'Who', 'E04') 44 | ] 45 | 46 | 47 | def '#method(T): inserts entity with compound PK'() { 48 | setup: 49 | def entity = entities[0] 50 | when: 51 | repository./$method/(entity) 52 | then: 53 | repository.findOne(entity.id) == entity 54 | where: 55 | method << ['save', 'insert'] 56 | } 57 | 58 | def '#method(T): updates entity with compound PK'() { 59 | setup: 60 | repository.save(entities[0]) 61 | def entity = repository.save(entities[1]) 62 | and: 63 | entity.passenger = 'Jameson' 64 | entity.seat = 'C03' 65 | when: 66 | repository./$method/(entity) 67 | then: 68 | repository.count() == 2 69 | repository.findOne(entity.id) == new BoardingPass('FOO-100', 2, 'Jameson', 'C03') 70 | where: 71 | method << ['save', 'update'] 72 | } 73 | 74 | 75 | def 'delete(ID): deletes entity by given compound PK'() { 76 | setup: 77 | def entity = entities[0] 78 | repository.save(entities) 79 | when: 80 | repository.delete(entity.id) 81 | then: 82 | ! repository.exists(entity.id) 83 | repository.count() == 3 84 | } 85 | 86 | def 'delete(T): deletes given entity with compound PK'() { 87 | setup: 88 | def entity = entities[0] 89 | repository.save(entities) 90 | when: 91 | repository.delete(entity) 92 | then: 93 | ! repository.exists(entity.id) 94 | repository.count() == 3 95 | } 96 | 97 | 98 | def 'findAll(Sortable): returns sorted entities'() { 99 | setup: 100 | repository.save(entities) 101 | when: 102 | def results = repository.findAll( 103 | new Sort(new Order(ASC, 'flight_no'), new Order(DESC, 'seq_no'))) 104 | then: 105 | results == entities.reverse() 106 | } 107 | 108 | def 'findAll(Pageable): returns paged and sorted entities'() { 109 | setup: 110 | repository.save(entities) 111 | when: 112 | def page = repository.findAll( 113 | new PageRequest(pageNum, 3, 114 | new Sort(new Order(ASC, 'flight_no'), new Order(DESC, 'seq_no')) 115 | )) 116 | then: 117 | page.totalElements == 4 118 | page.totalPages == 2 119 | page.content == entities[entitiesIdx] 120 | where: 121 | pageNum || entitiesIdx 122 | 0 || 3..1 123 | 1 || 0..0 124 | } 125 | 126 | 127 | def 'findAll(Iterable): returns nothing when given empty list'() { 128 | setup: 129 | repository.save(entities) 130 | when: 131 | def results = repository.findAll([]) 132 | then: 133 | results.asList().isEmpty() 134 | } 135 | 136 | def 'findAll(Iterable): returns one entity when given one id'() { 137 | setup: 138 | def ids = entities.collect { repository.save(it).id } 139 | when: 140 | def results = repository.findAll([ids[1]]) 141 | then: 142 | results.size() == 1 143 | results == [entities[1]] 144 | } 145 | 146 | def 'findAll(Iterable): returns two entities when given two ids'() { 147 | setup: 148 | def ids = entities.collect { repository.save(it).id } 149 | when: 150 | def results = repository.findAll(ids[1..2]) 151 | then: 152 | results.size() == 2 153 | results as Set == entities[1..2] as Set 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/sql/DefaultSqlGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.sql; 18 | 19 | import cz.jirutka.spring.data.jdbc.TableDescription; 20 | import org.springframework.data.domain.Pageable; 21 | import org.springframework.data.domain.Sort; 22 | import org.springframework.data.domain.Sort.Direction; 23 | import org.springframework.data.domain.Sort.Order; 24 | import org.springframework.util.Assert; 25 | 26 | import java.sql.DatabaseMetaData; 27 | import java.sql.SQLException; 28 | import java.util.Collection; 29 | import java.util.Iterator; 30 | import java.util.List; 31 | import java.util.Map; 32 | 33 | import static cz.jirutka.spring.data.jdbc.internal.StringUtils.repeat; 34 | import static java.lang.String.format; 35 | import static org.springframework.util.StringUtils.collectionToDelimitedString; 36 | 37 | /** 38 | * SQL Generator compatible with SQL:99. 39 | */ 40 | public class DefaultSqlGenerator implements SqlGenerator { 41 | 42 | static final String 43 | AND = " AND ", 44 | OR = " OR ", 45 | COMMA = ", ", 46 | PARAM = " = ?"; 47 | 48 | 49 | public boolean isCompatible(DatabaseMetaData metadata) throws SQLException { 50 | return true; 51 | } 52 | 53 | 54 | public String count(TableDescription table) { 55 | return format("SELECT count(*) FROM %s", table.getFromClause()); 56 | } 57 | 58 | public String deleteAll(TableDescription table) { 59 | return format("DELETE FROM %s", table.getTableName()); 60 | } 61 | 62 | public String deleteById(TableDescription table) { 63 | return deleteByIds(table, 1); 64 | } 65 | 66 | public String deleteByIds(TableDescription table, int idsCount) { 67 | return deleteAll(table) + " WHERE " + idsPredicate(table, idsCount); 68 | } 69 | 70 | public String existsById(TableDescription table) { 71 | return format("SELECT 1 FROM %s WHERE %s", table.getTableName(), idPredicate(table)); 72 | } 73 | 74 | public String insert(TableDescription table, Map columns) { 75 | 76 | return format("INSERT INTO %s (%s) VALUES (%s)", 77 | table.getTableName(), 78 | collectionToDelimitedString(columns.keySet(), COMMA), 79 | repeat("?", COMMA, columns.size())); 80 | } 81 | 82 | public String selectAll(TableDescription table) { 83 | return format("SELECT %s FROM %s", table.getSelectClause(), table.getFromClause()); 84 | } 85 | 86 | public String selectAll(TableDescription table, Pageable page) { 87 | Sort sort = page.getSort() != null ? page.getSort() : sortById(table); 88 | 89 | return format("SELECT t2__.* FROM ( " 90 | + "SELECT row_number() OVER (ORDER BY %s) AS rn__, t1__.* FROM ( %s ) t1__ " 91 | + ") t2__ WHERE t2__.rn__ BETWEEN %s AND %s", 92 | orderByExpression(sort), selectAll(table), 93 | page.getOffset() + 1, page.getOffset() + page.getPageSize()); 94 | } 95 | 96 | public String selectAll(TableDescription table, Sort sort) { 97 | return selectAll(table) + (sort != null ? orderByClause(sort) : ""); 98 | } 99 | 100 | public String selectById(TableDescription table) { 101 | return selectByIds(table, 1); 102 | } 103 | 104 | public String selectByIds(TableDescription table, int idsCount) { 105 | return idsCount > 0 106 | ? selectAll(table) + " WHERE " + idsPredicate(table, idsCount) 107 | : selectAll(table); 108 | } 109 | 110 | public String update(TableDescription table, Map columns) { 111 | 112 | return format("UPDATE %s SET %s WHERE %s", 113 | table.getTableName(), 114 | formatParameters(columns.keySet(), COMMA), 115 | idPredicate(table)); 116 | } 117 | 118 | 119 | protected String orderByClause(Sort sort) { 120 | return " ORDER BY " + orderByExpression(sort); 121 | } 122 | 123 | protected String orderByExpression(Sort sort) { 124 | StringBuilder sb = new StringBuilder(); 125 | 126 | for (Iterator it = sort.iterator(); it.hasNext(); ) { 127 | Order order = it.next(); 128 | sb.append(order.getProperty()).append(' ').append(order.getDirection()); 129 | 130 | if (it.hasNext()) sb.append(COMMA); 131 | } 132 | return sb.toString(); 133 | } 134 | 135 | protected Sort sortById(TableDescription table) { 136 | return new Sort(Direction.ASC, table.getPkColumns()); 137 | } 138 | 139 | 140 | private String idPredicate(TableDescription table) { 141 | return formatParameters(table.getPkColumns(), AND); 142 | } 143 | 144 | private String idsPredicate(TableDescription table, int idsCount) { 145 | Assert.isTrue(idsCount > 0, "idsCount must be greater than zero"); 146 | 147 | List idColumnNames = table.getPkColumns(); 148 | 149 | if (idsCount == 1) { 150 | return idPredicate(table); 151 | 152 | } else if (idColumnNames.size() > 1) { 153 | return repeat("(" + formatParameters(idColumnNames, AND) + ")", OR, idsCount); 154 | 155 | } else { 156 | return idColumnNames.get(0) + " IN (" + repeat("?", COMMA, idsCount) + ")"; 157 | } 158 | } 159 | 160 | private String formatParameters(Collection columns, String delimiter) { 161 | return collectionToDelimitedString(columns, delimiter, "", PARAM); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/JdbcRepositoryManyToOneIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License') 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc 18 | 19 | import cz.jirutka.spring.data.jdbc.fixtures.CommentWithUserRepository 20 | import cz.jirutka.spring.data.jdbc.fixtures.User 21 | import cz.jirutka.spring.data.jdbc.fixtures.UserRepository 22 | import cz.jirutka.spring.data.jdbc.fixtures.CommentWithUser 23 | import org.springframework.data.domain.PageRequest 24 | import org.springframework.data.domain.Sort 25 | import org.springframework.transaction.annotation.Transactional 26 | import spock.lang.Specification 27 | import spock.lang.Unroll 28 | 29 | import javax.annotation.Resource 30 | import java.sql.Date 31 | import java.sql.Timestamp 32 | 33 | import static java.util.Calendar.JANUARY 34 | import static org.springframework.data.domain.Sort.Direction.ASC 35 | import static org.springframework.data.domain.Sort.Direction.DESC 36 | 37 | @Unroll 38 | @Transactional 39 | abstract class JdbcRepositoryManyToOneIT extends Specification { 40 | 41 | @Resource CommentWithUserRepository repository 42 | @Resource UserRepository userRepository 43 | 44 | final someDate = new Date(new GregorianCalendar(2013, JANUARY, 19).timeInMillis) 45 | final someTimestamp = new Timestamp(new GregorianCalendar(2013, JANUARY, 20).timeInMillis) 46 | final someUser = new User('Jimmy', someDate, -1, false) 47 | 48 | final entities = [ 49 | new CommentWithUser(someUser, 'First comment', someTimestamp, 3), 50 | new CommentWithUser(someUser, 'Second comment', someTimestamp, 2), 51 | new CommentWithUser(someUser, 'Third comment', someTimestamp, 1) 52 | ] 53 | 54 | 55 | def setup() { 56 | userRepository.save(someUser) 57 | } 58 | 59 | 60 | def '#method(T): generates primary key'() { 61 | when: 62 | repository.save(entities[0]) 63 | then: 64 | entities[0].id != null 65 | where: 66 | method << ['save', 'insert'] 67 | } 68 | 69 | def '#method(T)/findOne(): inserts and returns entity with association attached'() { 70 | setup: 71 | def expected = entities[0] 72 | when: 73 | repository./$method/(expected) 74 | def actual = repository.findOne(expected.id) 75 | then: 76 | actual == expected 77 | actual.user == expected.user 78 | where: 79 | method << ['save', 'insert'] 80 | } 81 | 82 | def "#method(T): updates entity's association"() { 83 | setup: 84 | def firstUser = userRepository.save(new User('First user', someDate, 10, false)) 85 | def comment = repository.save(entities[0]) 86 | when: 87 | comment.user = firstUser 88 | repository./$method/(comment) 89 | then: 90 | repository.count() == 1 91 | def result = repository.findOne(comment.id) 92 | result.user == firstUser 93 | where: 94 | method << ['save', 'update'] 95 | } 96 | 97 | 98 | def 'findAll(Sort): returns sorted entities with the same association'() { 99 | setup: 100 | repository.save(entities) 101 | when: 102 | def actual = repository.findAll(new Sort('favourite_count')) 103 | then: 104 | actual == entities.sort{ it.favouriteCount } 105 | actual*.user == [someUser] * 3 106 | } 107 | 108 | def 'findAll(Sort): returns sorted entities with different associations'() { 109 | given: 110 | def firstUser = userRepository.save(new User('First user', someDate, 10, false)) 111 | def secondUser = userRepository.save(new User('Second user', someDate, 20, false)) 112 | def thirdUser = userRepository.save(new User('Third user', someDate, 30, false)) 113 | 114 | def first = repository.save(new CommentWithUser(firstUser, 'First comment', someTimestamp, 3)) 115 | def second = repository.save(new CommentWithUser(secondUser, 'Second comment', someTimestamp, 2)) 116 | def third = repository.save(new CommentWithUser(thirdUser, 'Third comment', someTimestamp, 1)) 117 | when: 118 | def results = repository.findAll(new Sort(DESC, 'favourite_count')) 119 | then: 120 | results == [first, second, third] 121 | } 122 | 123 | def 'findAll(Pageable): returns paged and sorted entities with associations attached'() { 124 | setup: 125 | repository.save(entities) 126 | when: 127 | def page = repository.findAll(new PageRequest(pageNum, 2, ASC, 'favourite_count')) 128 | then: 129 | page.totalElements == 3 130 | page.totalPages == 2 131 | page.content == entities[entitiesIdx] 132 | page.content*.user.unique() == [someUser] 133 | where: 134 | pageNum || entitiesIdx 135 | 0 || 2..1 136 | 1 || 0..0 137 | } 138 | 139 | 140 | def 'delete(T): deletes an entity without deleting associated entity'() { 141 | setup: 142 | def comment = repository.save(entities[0]) 143 | when: 144 | repository.delete(comment) 145 | then: 146 | repository.count() == 0 147 | userRepository.exists(someUser.userName) 148 | } 149 | 150 | 151 | def 'deletesAll(): deletes all entities without deleting associated entities'() { 152 | setup: 153 | repository.save(entities) 154 | when: 155 | repository.deleteAll() 156 | then: 157 | repository.count() == 0 158 | userRepository.exists(someUser.userName) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 4.0.0 6 | 7 | 8 | cz.jirutka.maven 9 | groovy-parent 10 | 1.3.2 11 | 12 | 13 | 14 | 15 | 16 | cz.jirutka.spring 17 | spring-data-jdbc-repository 18 | 0.6.0-SNAPSHOT 19 | jar 20 | 21 | Spring Data JDBC repository 22 | 23 | A repository implementation compatible with Spring Data abstraction that uses JdbcTemplate 24 | 25 | https://github.com/jirutka/spring-data-jdbc-repository 26 | 2012 27 | 28 | 29 | 30 | Jakub Jirutka 31 | jakub@jirutka.cz 32 | CTU in Prague 33 | http://www.cvut.cz 34 | 35 | 36 | Tomasz Nurkiewicz 37 | http://nurkiewicz.com 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Apache 2.0 47 | http://www.apache.org/licenses/LICENSE-2.0 48 | 49 | 50 | 51 | 52 | https://github.com/jirutka/spring-data-jdbc-repository 53 | scm:git:git@github.com:jirutka/spring-data-jdbc-repository.git 54 | 55 | 56 | 57 | github 58 | https://github.com/jirutka/spring-data-jdbc-repository/issues 59 | 60 | 61 | 62 | travis 63 | https://travis-ci.org/jirutka/spring-data-jdbc-repository/ 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | io.spring.platform 73 | platform-bom 74 | 2.0.2.RELEASE 75 | pom 76 | import 77 | 78 | 79 | 80 | 81 | 82 | 83 | org.springframework 84 | spring-beans 85 | 86 | 87 | 88 | org.springframework 89 | spring-jdbc 90 | 91 | 92 | 93 | org.springframework.data 94 | spring-data-commons 95 | 96 | 97 | 98 | 99 | 100 | cglib 101 | cglib-nodep 102 | 3.2.1 103 | test 104 | 105 | 106 | 107 | ch.qos.logback 108 | logback-classic 109 | test 110 | 111 | 112 | 113 | org.codehaus.groovy 114 | groovy 115 | test 116 | 117 | 118 | 119 | org.slf4j 120 | jcl-over-slf4j 121 | ${slf4j.version} 122 | test 123 | 124 | 125 | 126 | org.spockframework 127 | spock-core 128 | test 129 | 130 | 131 | 132 | org.spockframework 133 | spock-spring 134 | ${spock.version} 135 | 136 | 137 | org.codehaus.groovy 138 | groovy-all 139 | 140 | 141 | test 142 | 143 | 144 | 145 | org.springframework 146 | spring-aop 147 | test 148 | 149 | 150 | 151 | org.springframework 152 | spring-context 153 | test 154 | 155 | 156 | 157 | org.springframework 158 | spring-test 159 | test 160 | 161 | 162 | 163 | 164 | 165 | com.h2database 166 | h2 167 | test 168 | 169 | 170 | 171 | com.zaxxer 172 | HikariCP-java6 173 | test 174 | 175 | 176 | 177 | mysql 178 | mysql-connector-java 179 | test 180 | 181 | 182 | 183 | net.sourceforge.jtds 184 | jtds 185 | 1.3.1 186 | test 187 | 188 | 189 | 190 | org.apache.derby 191 | derby 192 | test 193 | 194 | 195 | 196 | org.hsqldb 197 | hsqldb 198 | test 199 | 200 | 201 | 202 | org.mariadb.jdbc 203 | mariadb-java-client 204 | 1.3.6 205 | test 206 | 207 | 208 | 209 | org.postgresql 210 | postgresql 211 | test 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | oracle 221 | 224 | 225 | 226 | com.oracle 227 | ojdbc6 228 | 11.2.0.3 229 | test 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/sql/SqlGeneratorTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License') 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc.sql 18 | 19 | import cz.jirutka.spring.data.jdbc.TableDescription 20 | import org.springframework.data.domain.PageRequest 21 | import org.springframework.data.domain.Pageable 22 | import org.springframework.data.domain.Sort 23 | import org.springframework.data.domain.Sort.Direction 24 | import org.springframework.data.domain.Sort.Order 25 | import spock.lang.Specification 26 | import spock.lang.Unroll 27 | 28 | import static org.springframework.data.domain.Sort.Direction.ASC 29 | import static org.springframework.data.domain.Sort.Direction.DESC 30 | 31 | @Unroll 32 | class SqlGeneratorTest extends Specification { 33 | 34 | final ANY = new Object() 35 | 36 | def table = new TableDescription ( 37 | tableName: 'tab', 38 | selectClause: 'a, b', 39 | fromClause: 'tabx', 40 | pkColumns: ['tid'] 41 | ) 42 | 43 | def getSqlGenerator() { new DefaultSqlGenerator() } 44 | 45 | 46 | def 'count()'() { 47 | expect: 48 | sqlGenerator.count(table) == 'SELECT count(*) FROM tabx' 49 | } 50 | 51 | 52 | def 'deleteAll()'() { 53 | expect: 54 | sqlGenerator.deleteAll(table) == 'DELETE FROM tab' 55 | } 56 | 57 | 58 | def 'deleteById(): with #desc'() { 59 | setup: 60 | table.pkColumns = pkColumns(pkSize) 61 | expect: 62 | sqlGenerator.deleteById(table) == "DELETE FROM tab WHERE ${pkPredicate(pkSize)}" 63 | where: 64 | pkSize || desc 65 | 1 || 'simple PK' 66 | 3 || 'composite PK' 67 | } 68 | 69 | 70 | def 'deleteByIds(): with idsCount = #idsCount'() { 71 | when: 72 | sqlGenerator.deleteByIds(table, idsCount) 73 | then: 74 | thrown IllegalArgumentException 75 | where: 76 | idsCount << [0, -1] 77 | } 78 | 79 | def 'deleteByIds(): when simple PK and given #desc'() { 80 | expect: 81 | sqlGenerator.deleteByIds(table, idsCount) == "DELETE FROM tab WHERE ${whereClause}" 82 | where: 83 | idsCount || whereClause | desc 84 | 1 || 'tid = ?' | 'one id' 85 | 3 || 'tid IN (?, ?, ?)' | 'several ids' 86 | } 87 | 88 | def 'deleteByIds(): when composite PK and given #desc'() { 89 | setup: 90 | table.pkColumns = pkColumns(2) 91 | expect: 92 | sqlGenerator.deleteByIds(table, idsCount) == "DELETE FROM tab WHERE ${whereClause}" 93 | where: 94 | idsCount || whereClause | desc 95 | 1 || pkPredicate(2) | 'one id' 96 | 2 || "(${pkPredicate(2)}) OR (${pkPredicate(2)})" | 'several ids' 97 | } 98 | 99 | 100 | def 'existsById(): with #desc'() { 101 | setup: 102 | table.pkColumns = pkColumns(pkSize) 103 | expect: 104 | sqlGenerator.existsById(table) == "SELECT 1 FROM tab WHERE ${pkPredicate(pkSize)}" 105 | where: 106 | pkSize || desc 107 | 1 || 'simple PK' 108 | 3 || 'composite PK' 109 | } 110 | 111 | 112 | def 'insert()'() { 113 | when: 114 | def actual = sqlGenerator.insert(table, [x: ANY, y: ANY, z: ANY]) 115 | then: 116 | actual == 'INSERT INTO tab (x, y, z) VALUES (?, ?, ?)' 117 | } 118 | 119 | 120 | def 'selectAll()'() { 121 | expect: 122 | sqlGenerator.selectAll(table) == 'SELECT a, b FROM tabx' 123 | } 124 | 125 | def "selectAll(Pageable): #desc"() { 126 | setup: 127 | table.pkColumns = pkColumns(pkSize) 128 | def expected = expectedPaginatedQuery(table, pageable) 129 | expect: 130 | sqlGenerator.selectAll(table, pageable) == expected 131 | where: 132 | pkSize | pageable || desc 133 | 1 | page(0, 10) || 'when simple key and requested first page' 134 | 1 | page(20, 10) || 'when simple key and requested third page' 135 | 1 | page(0, 10, order(ASC, 'a')) || 'when simple key and requested first page with sort' 136 | 3 | page(0, 10) || 'when composite key and requested first page' 137 | 3 | page(20, 10, order(ASC, 'a')) || 'when composite key and requested third page with sort' 138 | } 139 | 140 | def 'selectAll(Sort): #expected'() { 141 | when: 142 | def actual = sqlGenerator.selectAll(table, new Sort(orders)) 143 | then: 144 | actual == "SELECT a, b FROM tabx ${expected}" 145 | where: 146 | orders || expected 147 | [order(ASC, 'a')] || 'ORDER BY a ASC' 148 | [order(DESC, 'a')] || 'ORDER BY a DESC' 149 | [order(ASC, 'a'), order(DESC, 'b')] || 'ORDER BY a ASC, b DESC' 150 | } 151 | 152 | 153 | def 'selectById(): with #desc'() { 154 | setup: 155 | table.pkColumns = pkColumns(pkSize) 156 | expect: 157 | sqlGenerator.selectById(table) == "SELECT a, b FROM tabx WHERE ${pkPredicate(pkSize)}" 158 | where: 159 | pkSize || desc 160 | 1 || 'simple PK' 161 | 3 || 'composite PK' 162 | } 163 | 164 | 165 | def 'selectByIds(): when simple PK and given #desc'() { 166 | expect: 167 | sqlGenerator.selectByIds(table, idsCount) == "SELECT a, b FROM tabx${expected}" 168 | where: 169 | idsCount || expected | desc 170 | 0 || '' | 'no id' 171 | 1 || ' WHERE tid = ?' | 'one id' 172 | 2 || ' WHERE tid IN (?, ?)' | 'two ids' 173 | 3 || ' WHERE tid IN (?, ?, ?)' | 'several ids' 174 | } 175 | 176 | def 'selectByIds(): when composite PK and given #desc'() { 177 | setup: 178 | table.pkColumns = pkColumns(3) 179 | expected = expected.replaceAll('%1', pkPredicate(3)) 180 | when: 181 | def actual = sqlGenerator.selectByIds(table, idsCount) 182 | then: 183 | actual == "SELECT a, b FROM tabx${expected}" 184 | where: 185 | idsCount || expected | desc 186 | 0 || '' | 'no id' 187 | 1 || ' WHERE %1' | 'one id' 188 | 2 || ' WHERE (%1) OR (%1)' | 'two ids' 189 | 3 || ' WHERE (%1) OR (%1) OR (%1)' | 'several ids' 190 | } 191 | 192 | 193 | def 'update(): with #desc'() { 194 | setup: 195 | table.pkColumns = pkColumns(idsCount) 196 | when: 197 | def actual = sqlGenerator.update(table, [x: ANY, y: ANY, z: ANY]) 198 | then: 199 | actual == "UPDATE tab SET x = ?, y = ?, z = ? WHERE ${pkPredicate(idsCount)}" 200 | where: 201 | idsCount || desc 202 | 1 || 'simple PK' 203 | 2 || 'composite PK' 204 | } 205 | 206 | 207 | def expectedPaginatedQuery(TableDescription table, Pageable page) { 208 | 209 | // If sort is not specified, then it should be sorted by primary key columns. 210 | def sort = page.sort ?: new Sort(ASC, table.pkColumns) 211 | 212 | def firstIndex = page.offset + 1 213 | def lastIndex = page.offset + page.pageSize 214 | 215 | """ 216 | SELECT t2__.* FROM ( 217 | SELECT row_number() OVER (${orderBy(sort)}) AS rn__, t1__.* FROM ( 218 | SELECT ${table.selectClause} FROM ${table.fromClause} 219 | ) t1__ 220 | ) t2__ WHERE t2__.rn__ BETWEEN ${firstIndex} AND ${lastIndex} 221 | """.trim().replaceAll(/\s+/, ' ') 222 | } 223 | 224 | 225 | def page(int offset, int limit, Order... orders) { 226 | def sort = orders.length > 0 ? new Sort(orders) : null 227 | new PageRequest(offset / limit as int, limit, sort) 228 | } 229 | 230 | def order(Direction dir, String property) { 231 | new Order(dir, property) 232 | } 233 | 234 | def orderBy(Sort sort) { 235 | 'ORDER BY ' + sort.collect { "${it.property} ${it.direction.name()}" }.join(', ') 236 | } 237 | 238 | static pkColumns(count) { 239 | (1..count).collect { "id${it}" }*.toString() 240 | } 241 | 242 | static pkPredicate(count) { 243 | pkColumns(count).collect { "$it = ?" }.join(' AND ') 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/data/jdbc/JdbcRepositoryManualKeyIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License') 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc 18 | 19 | import cz.jirutka.spring.data.jdbc.fixtures.CommentRepository 20 | import cz.jirutka.spring.data.jdbc.fixtures.User 21 | import cz.jirutka.spring.data.jdbc.fixtures.UserRepository 22 | import org.springframework.dao.DuplicateKeyException 23 | import org.springframework.data.domain.PageRequest 24 | import org.springframework.data.domain.Sort 25 | import org.springframework.data.domain.Sort.Order 26 | import org.springframework.jdbc.core.JdbcTemplate 27 | import org.springframework.transaction.annotation.Transactional 28 | import spock.lang.Specification 29 | import spock.lang.Unroll 30 | 31 | import javax.annotation.Resource 32 | import javax.sql.DataSource 33 | import java.sql.Date 34 | 35 | import static org.springframework.data.domain.Sort.Direction.ASC 36 | import static org.springframework.data.domain.Sort.Direction.DESC 37 | 38 | @Unroll 39 | @Transactional 40 | abstract class JdbcRepositoryManualKeyIT extends Specification { 41 | 42 | @Resource UserRepository repository 43 | @Resource CommentRepository commentRepository 44 | @Resource DataSource dataSource 45 | 46 | final someDateOfBirth = new Date(new GregorianCalendar(2013, Calendar.JANUARY, 9).timeInMillis) 47 | 48 | final entities = [ 49 | Ruby: new User('Ruby', someDateOfBirth, 40, true), 50 | Emma: new User('Emma', someDateOfBirth, 38, true), 51 | Drew: new User('Drew', someDateOfBirth, 40, true), 52 | Lucy: new User('Lucy', someDateOfBirth, 38, true), 53 | Mindy: new User('Mindy', someDateOfBirth, 42, true) 54 | ] 55 | 56 | JdbcTemplate jdbc 57 | 58 | 59 | def setup() { 60 | jdbc = new JdbcTemplate(dataSource) 61 | 62 | for (User user : entities.values()) { 63 | insertUser(user) 64 | } 65 | assert selectIds() == entities.keySet() 66 | } 67 | 68 | 69 | def 'findOne(ID): returns null when the table is empty'() { 70 | setup: 71 | deleteAllUsers() 72 | expect: 73 | repository.findOne('Emma') == null 74 | } 75 | 76 | def 'findOne(ID): returns null when record for given id does not exist'() { 77 | expect: 78 | repository.findOne('John') == null 79 | } 80 | 81 | def 'findOne(ID): returns entity for the given id'() { 82 | expect: 83 | repository.findOne('Drew') == entities['Drew'] 84 | } 85 | 86 | 87 | def 'findAll(): returns empty list when the table is empty'() { 88 | setup: 89 | deleteAllUsers() 90 | expect: 91 | repository.findAll().empty 92 | } 93 | 94 | def 'findAll(): returns list of all entities in the table'() { 95 | expect: 96 | repository.findAll() as Set == entities.values() as Set 97 | } 98 | 99 | 100 | def 'findAll(Iterable): returns list of entities for the given ids'() { 101 | when: 102 | def results = repository.findAll(ids) as Set 103 | then: 104 | results == ids.collect(entities.&get) as Set 105 | where: 106 | ids << [[], ['Mindy'], ['Mindy', 'Ruby']] 107 | } 108 | 109 | 110 | def 'findAll(Sort): returns entities sorted by one column'() { 111 | when: 112 | def results = repository.findAll(new Sort(new Order(ASC, 'user_name'))) 113 | then: 114 | results == ['Drew', 'Emma', 'Lucy', 'Mindy', 'Ruby'].collect(entities.&get) 115 | } 116 | 117 | def 'findAll(Sort): returns entities sorted by two columns'() { 118 | when: 119 | def results = repository.findAll( 120 | new Sort(new Order(DESC, 'reputation'), new Order(ASC, 'user_name'))) 121 | then: 122 | results == ['Mindy', 'Drew', 'Ruby', 'Emma', 'Lucy'].collect(entities.&get) 123 | } 124 | 125 | 126 | def 'findAll(Pageable): returns empty page when the table is empty'() { 127 | setup: 128 | deleteAllUsers() 129 | when: 130 | def page = repository.findAll(new PageRequest(0, 20)) 131 | then: 132 | ! page.hasContent() 133 | page.totalElements == 0 134 | page.size == 20 135 | page.number == 0 136 | } 137 | 138 | def 'findAll(Pageable): returns paged entities'() { 139 | when: 140 | def page = repository.findAll(new PageRequest(pageNum, 3)) 141 | then: 142 | page.totalElements == entities.size() 143 | page.size == 3 144 | page.number == pageNum 145 | page.content.size() == resultSize 146 | entities.values().containsAll(page.content) 147 | where: 148 | pageNum | resultSize 149 | 0 | 3 150 | 1 | 2 151 | } 152 | 153 | def 'findAll(Pageable): returns empty page when 2nd page requested, but only one record in table'() { 154 | setup: 155 | deleteAllUsers() 156 | insertUser entities['Mindy'] 157 | when: 158 | def page = repository.findAll(new PageRequest(1, 5)) 159 | then: 160 | page.content.size() == 0 161 | page.number == 1 162 | page.size == 5 163 | page.totalElements == 1 164 | } 165 | 166 | def 'findAll(Pageable): returns paged entities sorted by two columns'() { 167 | when: 168 | def page = repository.findAll(new PageRequest(pageNum, 3, 169 | new Sort(new Order(DESC, 'reputation'), new Order(ASC, 'user_name')))) 170 | then: 171 | page.number == pageNum 172 | page.size == 3 173 | page.totalElements == 5 174 | page.content == resultIds.collect(entities.&get) 175 | where: 176 | pageNum | resultIds 177 | 0 | ['Mindy', 'Drew', 'Ruby'] 178 | 1 | ['Emma', 'Lucy'] 179 | } 180 | 181 | 182 | def '#method(T): inserts the given new entity'() { 183 | setup: 184 | deleteAllUsers() 185 | when: 186 | repository./$method/(entities['Mindy']) 187 | then: 188 | selectUserById('Mindy') == entities['Mindy'] 189 | where: 190 | method << ['save', 'insert'] 191 | } 192 | 193 | def 'save(T): throws DuplicateKeyException when the given entity is marked as new, but already exists'() { 194 | when: 195 | repository.save(entities['Mindy']) 196 | then: 197 | thrown DuplicateKeyException 198 | } 199 | 200 | def 'insert(): throws DuplicateKeyException when given entity that is already persisted'() { 201 | when: 202 | repository.save(entities['Mindy']) 203 | then: 204 | thrown DuplicateKeyException 205 | } 206 | 207 | def '#method(T): updates the record when already exists'() { 208 | setup: 209 | deleteAllUsers() 210 | and: 211 | def entity = repository.save(entities['Lucy']) 212 | entity.enabled = false 213 | entity.reputation = 42 214 | when: 215 | repository./$method/(entity) 216 | then: 217 | repository.findOne(entity.id) == new User(entity.id, entity.dateOfBirth, 42, false) 218 | where: 219 | method << ['save', 'update'] 220 | } 221 | 222 | def 'update(): throws NoRecordUpdatedException when record does not exist'() { 223 | setup: 224 | deleteAllUsers() 225 | when: 226 | repository.update(entities['Emma']) 227 | then: 228 | def ex = thrown(NoRecordUpdatedException) 229 | ex.tableName == repository.tableDesc.tableName 230 | ex.id == ['Emma'] 231 | } 232 | 233 | def 'update(): throws IllegalArgumentException when given entity with null id'() { 234 | setup: 235 | deleteAllUsers() 236 | when: 237 | repository.update(new User(null, someDateOfBirth, 0, true)) 238 | then: 239 | thrown IllegalArgumentException 240 | } 241 | 242 | 243 | def 'save(Iterable): inserts given entities'() { 244 | setup: 245 | deleteAllUsers() 246 | when: 247 | repository.save(entities.values()) 248 | then: 249 | entities.values().every { User expected -> 250 | selectUserById(expected.id) == expected 251 | } 252 | } 253 | 254 | 255 | def 'exists(ID): returns false when DB is empty'() { 256 | setup: 257 | deleteAllUsers() 258 | expect: 259 | ! repository.exists('John') 260 | } 261 | 262 | def 'exists(ID): returns false when record with such id does not exist'() { 263 | expect: 264 | ! repository.exists('John') 265 | } 266 | 267 | def 'exists(ID): returns true when record for given id exists'() { 268 | expect: 269 | repository.exists('Mindy') 270 | } 271 | 272 | 273 | def 'delete(ID): does nothing when record for given id does not exist'() { 274 | when: 275 | repository.delete('Johny') 276 | then: 277 | notThrown Exception 278 | } 279 | 280 | def 'delete(ID): deletes record by given id'() { 281 | when: 282 | repository.delete('Lucy') 283 | then: 284 | ! selectIds().contains('Lucy') 285 | selectIds() == entities.keySet() - 'Lucy' 286 | } 287 | 288 | def 'delete(T): deletes record by given entity'() { 289 | when: 290 | repository.delete(entities['Lucy']) 291 | then: 292 | ! selectIds().contains('Lucy') 293 | selectIds() == entities.keySet() - 'Lucy' 294 | } 295 | 296 | def 'delete(Iterable): deletes multiple entities'() { 297 | setup: 298 | def toDelete = ['Lucy', 'Emma'].collect(entities.&get) 299 | when: 300 | repository.delete(toDelete as List) 301 | then: 302 | selectIds() == entities.keySet() - toDelete*.id 303 | } 304 | 305 | def 'delete(Iterable): ignores non existing entities'() { 306 | setup: 307 | def toDelete = [entities['Lucy'], new User('John', someDateOfBirth, 15, true)] 308 | when: 309 | repository.delete(toDelete as List) 310 | then: 311 | selectIds() == entities.keySet() - toDelete*.id 312 | } 313 | 314 | 315 | def 'deleteAll(): deletes all records in the table'() { 316 | when: 317 | repository.deleteAll() 318 | then: 319 | selectIds().empty 320 | } 321 | 322 | 323 | def 'count(): returns zero when DB is empty'() { 324 | setup: 325 | deleteAllUsers() 326 | expect: 327 | repository.count() == 0 328 | } 329 | 330 | def 'count(): returns correct number of records in the table'() { 331 | expect: 332 | repository.count() == 5 333 | } 334 | 335 | 336 | 337 | def insertUser(User user) { 338 | jdbc.update('INSERT INTO USERS VALUES (?, ?, ?, ?)', 339 | user.userName, user.dateOfBirth, user.reputation, user.enabled) 340 | assert selectIds().contains(user.userName) 341 | } 342 | 343 | def selectUserById(String id) { 344 | jdbc.queryForObject('SELECT * FROM USERS WHERE user_name = ?', UserRepository.ROW_MAPPER, id) 345 | } 346 | 347 | def selectIds() { 348 | jdbc.queryForList('SELECT user_name FROM USERS', String) as Set 349 | } 350 | 351 | def deleteAllUsers() { 352 | jdbc.execute('DELETE FROM USERS') 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/data/jdbc/BaseJdbcRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 Tomasz Nurkiewicz . 3 | * Copyright 2016 Jakub Jirutka . 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package cz.jirutka.spring.data.jdbc; 18 | 19 | import cz.jirutka.spring.data.jdbc.sql.SqlGenerator; 20 | import cz.jirutka.spring.data.jdbc.sql.SqlGeneratorFactory; 21 | import org.springframework.beans.factory.InitializingBean; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.core.GenericTypeResolver; 24 | import org.springframework.data.domain.Page; 25 | import org.springframework.data.domain.PageImpl; 26 | import org.springframework.data.domain.Pageable; 27 | import org.springframework.data.domain.Persistable; 28 | import org.springframework.data.domain.Sort; 29 | import org.springframework.data.repository.PagingAndSortingRepository; 30 | import org.springframework.data.repository.core.EntityInformation; 31 | import org.springframework.data.repository.core.support.PersistableEntityInformation; 32 | import org.springframework.data.repository.core.support.ReflectionEntityInformation; 33 | import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException; 34 | import org.springframework.jdbc.core.JdbcOperations; 35 | import org.springframework.jdbc.core.JdbcTemplate; 36 | import org.springframework.jdbc.core.PreparedStatementCreator; 37 | import org.springframework.jdbc.core.RowMapper; 38 | import org.springframework.jdbc.support.GeneratedKeyHolder; 39 | import org.springframework.util.Assert; 40 | import org.springframework.util.LinkedCaseInsensitiveMap; 41 | 42 | import javax.sql.DataSource; 43 | import java.io.Serializable; 44 | import java.sql.Connection; 45 | import java.sql.PreparedStatement; 46 | import java.sql.SQLException; 47 | import java.util.ArrayList; 48 | import java.util.Collections; 49 | import java.util.List; 50 | import java.util.Map; 51 | 52 | import static cz.jirutka.spring.data.jdbc.internal.IterableUtils.toList; 53 | import static cz.jirutka.spring.data.jdbc.internal.ObjectUtils.wrapToArray; 54 | import static java.util.Arrays.asList; 55 | 56 | /** 57 | * Implementation of {@link PagingAndSortingRepository} using {@link JdbcTemplate} 58 | */ 59 | public abstract class BaseJdbcRepository 60 | implements JdbcRepository, InitializingBean { 61 | 62 | private final EntityInformation entityInfo; 63 | private final TableDescription table; 64 | private final RowMapper rowMapper; 65 | private final RowUnmapper rowUnmapper; 66 | 67 | // Read-only after initialization (invoking afterPropertiesSet()). 68 | private DataSource dataSource; 69 | private JdbcOperations jdbcOps; 70 | private SqlGeneratorFactory sqlGeneratorFactory = SqlGeneratorFactory.getInstance(); 71 | private SqlGenerator sqlGenerator; 72 | 73 | private boolean initialized; 74 | 75 | 76 | public BaseJdbcRepository(EntityInformation entityInformation, RowMapper rowMapper, 77 | RowUnmapper rowUnmapper, TableDescription table) { 78 | Assert.notNull(rowMapper); 79 | Assert.notNull(table); 80 | 81 | this.entityInfo = entityInformation != null ? entityInformation : createEntityInformation(); 82 | this.rowUnmapper = rowUnmapper != null ? rowUnmapper : new UnsupportedRowUnmapper(); 83 | this.rowMapper = rowMapper; 84 | this.table = table; 85 | } 86 | 87 | public BaseJdbcRepository(RowMapper rowMapper, RowUnmapper rowUnmapper, TableDescription table) { 88 | this(null, rowMapper, rowUnmapper, table); 89 | } 90 | 91 | public BaseJdbcRepository(RowMapper rowMapper, RowUnmapper rowUnmapper, String tableName, String idColumn) { 92 | this(rowMapper, rowUnmapper, new TableDescription(tableName, idColumn)); 93 | } 94 | 95 | public BaseJdbcRepository(RowMapper rowMapper, RowUnmapper rowUnmapper, String tableName) { 96 | this(rowMapper, rowUnmapper, new TableDescription(tableName, "id")); 97 | } 98 | 99 | 100 | @Override 101 | public void afterPropertiesSet() { 102 | Assert.notNull(dataSource, "dataSource must be provided"); 103 | 104 | if (jdbcOps == null) { 105 | jdbcOps = new JdbcTemplate(dataSource); 106 | } 107 | if (sqlGenerator == null) { 108 | sqlGenerator = sqlGeneratorFactory.getGenerator(dataSource); 109 | } 110 | initialized = true; 111 | } 112 | 113 | /** 114 | * @param dataSource The DataSource to use (required). 115 | * @throws IllegalStateException if invoked after initialization 116 | * (i.e. after {@link #afterPropertiesSet()} has been invoked). 117 | */ 118 | @Autowired 119 | public void setDataSource(DataSource dataSource) { 120 | throwOnChangeAfterInitialization("dataSource"); 121 | this.dataSource = dataSource; 122 | } 123 | 124 | /** 125 | * @param jdbcOps If not set, {@link JdbcTemplate} is created. 126 | * @throws IllegalStateException if invoked after initialization 127 | * (i.e. after {@link #afterPropertiesSet()} has been invoked). 128 | */ 129 | @Autowired(required = false) 130 | public void setJdbcOperations(JdbcOperations jdbcOps) { 131 | throwOnChangeAfterInitialization("jdbcOperations"); 132 | this.jdbcOps = jdbcOps; 133 | } 134 | 135 | /** 136 | * @param sqlGeneratorFactory If not set, {@link SqlGeneratorFactory#getInstance()} 137 | * is used. 138 | * @throws IllegalStateException if invoked after initialization 139 | * (i.e. after {@link #afterPropertiesSet()} has been invoked). 140 | */ 141 | @Autowired(required = false) 142 | public void setSqlGeneratorFactory(SqlGeneratorFactory sqlGeneratorFactory) { 143 | throwOnChangeAfterInitialization("sqlGeneratorFactory"); 144 | this.sqlGeneratorFactory = sqlGeneratorFactory; 145 | } 146 | 147 | /** 148 | * @param sqlGenerator If not set, then it's obtained from 149 | * {@link SqlGeneratorFactory}. 150 | * @throws IllegalStateException if invoked after initialization 151 | * (i.e. after {@link #afterPropertiesSet()} has been invoked). 152 | */ 153 | @Autowired(required = false) 154 | public void setSqlGenerator(SqlGenerator sqlGenerator) { 155 | throwOnChangeAfterInitialization("sqlGenerator"); 156 | this.sqlGenerator = sqlGenerator; 157 | } 158 | 159 | 160 | ////////// Repository methods ////////// 161 | 162 | @Override 163 | public long count() { 164 | return jdbcOps.queryForObject(sqlGenerator.count(table), Long.class); 165 | } 166 | 167 | @Override 168 | public void delete(ID id) { 169 | // Workaround for Groovy that cannot distinguish between two methods 170 | // with almost the same type erasure and always calls the former one. 171 | if (getEntityInfo().getJavaType().isInstance(id)) { 172 | // noinspection unchecked 173 | id = id((T) id); 174 | } 175 | jdbcOps.update(sqlGenerator.deleteById(table), wrapToArray(id)); 176 | } 177 | 178 | @Override 179 | public void delete(T entity) { 180 | delete(id(entity)); 181 | } 182 | 183 | @Override 184 | public void delete(Iterable entities) { 185 | List ids = ids(entities); 186 | 187 | if (!ids.isEmpty()) { 188 | jdbcOps.update(sqlGenerator.deleteByIds(table, ids.size()), flatten(ids)); 189 | } 190 | } 191 | 192 | @Override 193 | public void deleteAll() { 194 | jdbcOps.update(sqlGenerator.deleteAll(table)); 195 | } 196 | 197 | @Override 198 | public boolean exists(ID id) { 199 | return !jdbcOps.queryForList( 200 | sqlGenerator.existsById(table), wrapToArray(id), Integer.class).isEmpty(); 201 | } 202 | 203 | @Override 204 | public List findAll() { 205 | return jdbcOps.query(sqlGenerator.selectAll(table), rowMapper); 206 | } 207 | 208 | @Override 209 | public T findOne(ID id) { 210 | List entityOrEmpty = jdbcOps.query( 211 | sqlGenerator.selectById(table), wrapToArray(id), rowMapper); 212 | 213 | return entityOrEmpty.isEmpty() ? null : entityOrEmpty.get(0); 214 | } 215 | 216 | @Override 217 | public S save(S entity) { 218 | return getEntityInfo().isNew(entity) ? insert(entity) : update(entity); 219 | } 220 | 221 | @Override 222 | public List save(Iterable entities) { 223 | List ret = new ArrayList<>(); 224 | for (S s : entities) { 225 | ret.add(save(s)); 226 | } 227 | return ret; 228 | } 229 | 230 | @Override 231 | public List findAll(Iterable ids) { 232 | List idsList = toList(ids); 233 | 234 | if (idsList.isEmpty()) { 235 | return Collections.emptyList(); 236 | } 237 | return jdbcOps.query( 238 | sqlGenerator.selectByIds(table, idsList.size()), rowMapper, flatten(idsList)); 239 | } 240 | 241 | @Override 242 | public List findAll(Sort sort) { 243 | return jdbcOps.query(sqlGenerator.selectAll(table, sort), rowMapper); 244 | } 245 | 246 | @Override 247 | public Page findAll(Pageable page) { 248 | String query = sqlGenerator.selectAll(table, page); 249 | 250 | return new PageImpl<>(jdbcOps.query(query, rowMapper), page, count()); 251 | } 252 | 253 | @Override 254 | public S insert(S entity) { 255 | Map columns = preInsert(columns(entity), entity); 256 | 257 | return id(entity) == null 258 | ? insertWithAutoGeneratedKey(entity, columns) 259 | : insertWithManuallyAssignedKey(entity, columns); 260 | } 261 | 262 | @Override 263 | public S update(S entity) { 264 | Map columns = preUpdate(entity, columns(entity)); 265 | 266 | List idValues = removeIdColumns(columns); // modifies the columns list! 267 | String updateQuery = sqlGenerator.update(table, columns); 268 | 269 | if (idValues.contains(null)) { 270 | throw new IllegalArgumentException("Entity's ID contains null values"); 271 | } 272 | 273 | for (int i = 0; i < table.getPkColumns().size(); i++) { 274 | columns.put(table.getPkColumns().get(i), idValues.get(i)); 275 | } 276 | Object[] queryParams = columns.values().toArray(); 277 | 278 | int rowsAffected = jdbcOps.update(updateQuery, queryParams); 279 | 280 | if (rowsAffected < 1) { 281 | throw new NoRecordUpdatedException(table.getTableName(), idValues.toArray()); 282 | } 283 | if (rowsAffected > 1) { 284 | throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(updateQuery, 1, rowsAffected); 285 | } 286 | 287 | return postUpdate(entity); 288 | } 289 | 290 | 291 | ////////// Getters ////////// 292 | 293 | protected EntityInformation getEntityInfo() { 294 | return entityInfo; 295 | } 296 | 297 | protected JdbcOperations getJdbcOperations() { 298 | return jdbcOps; 299 | } 300 | 301 | protected SqlGenerator getSqlGenerator() { 302 | return sqlGenerator; 303 | } 304 | 305 | protected TableDescription getTableDesc() { 306 | return table; 307 | } 308 | 309 | protected JdbcOperations jdbc() { 310 | return jdbcOps; 311 | } 312 | 313 | 314 | ////////// Hooks ////////// 315 | 316 | protected Map preInsert(Map columns, T entity) { 317 | return columns; 318 | } 319 | 320 | /** 321 | * General purpose hook method that is called every time {@link #insert} is called with a new entity. 322 | *

323 | * OVerride this method e.g. if you want to fetch auto-generated key from database 324 | * 325 | * 326 | * @param entity Entity that was passed to {@link #insert} 327 | * @param generatedId ID generated during INSERT or NULL if not available/not generated. 328 | * TODO: Type should be ID, not Number 329 | * @return Either the same object as an argument or completely different one 330 | */ 331 | protected S postInsert(S entity, Number generatedId) { 332 | return entity; 333 | } 334 | 335 | protected Map preUpdate(T entity, Map columns) { 336 | return columns; 337 | } 338 | 339 | /** 340 | * General purpose hook method that is called every time {@link #update} is called. 341 | * 342 | * @param entity The entity that was passed to {@link #update}. 343 | * @return Either the same object as an argument or completely different one. 344 | */ 345 | protected S postUpdate(S entity) { 346 | return entity; 347 | } 348 | 349 | 350 | private ID id(T entity) { 351 | return getEntityInfo().getId(entity); 352 | } 353 | 354 | private List ids(Iterable entities) { 355 | List ids = new ArrayList<>(); 356 | 357 | for (T entity : entities) { 358 | ids.add(id(entity)); 359 | } 360 | return ids; 361 | } 362 | 363 | private S insertWithManuallyAssignedKey(S entity, Map columns) { 364 | String insertQuery = sqlGenerator.insert(table, columns); 365 | Object[] queryParams = columns.values().toArray(); 366 | 367 | jdbcOps.update(insertQuery, queryParams); 368 | 369 | return postInsert(entity, null); 370 | } 371 | 372 | private S insertWithAutoGeneratedKey(S entity, Map columns) { 373 | removeIdColumns(columns); 374 | 375 | final String insertQuery = sqlGenerator.insert(table, columns); 376 | final Object[] queryParams = columns.values().toArray(); 377 | final GeneratedKeyHolder key = new GeneratedKeyHolder(); 378 | 379 | jdbcOps.update(new PreparedStatementCreator() { 380 | public PreparedStatement createPreparedStatement(Connection con) throws SQLException { 381 | String idColumnName = table.getPkColumns().get(0); 382 | PreparedStatement ps = con.prepareStatement(insertQuery, new String[]{idColumnName}); 383 | for (int i = 0; i < queryParams.length; ++i) { 384 | ps.setObject(i + 1, queryParams[i]); 385 | } 386 | return ps; 387 | } 388 | }, key); 389 | 390 | return postInsert(entity, key.getKey()); 391 | } 392 | 393 | private List removeIdColumns(Map columns) { 394 | List idColumnsValues = new ArrayList<>(columns.size()); 395 | 396 | for (String idColumn : table.getPkColumns()) { 397 | idColumnsValues.add(columns.remove(idColumn)); 398 | } 399 | return idColumnsValues; 400 | } 401 | 402 | private Map columns(T entity) { 403 | Map columns = new LinkedCaseInsensitiveMap<>(); 404 | columns.putAll(rowUnmapper.mapColumns(entity)); 405 | 406 | return columns; 407 | } 408 | 409 | private static Object[] flatten(List ids) { 410 | List result = new ArrayList<>(); 411 | for (ID id : ids) { 412 | result.addAll(asList(wrapToArray(id))); 413 | } 414 | return result.toArray(); 415 | } 416 | 417 | @SuppressWarnings("unchecked") 418 | private EntityInformation createEntityInformation() { 419 | 420 | Class entityType = (Class) GenericTypeResolver.resolveTypeArguments( 421 | getClass(), BaseJdbcRepository.class)[0]; 422 | 423 | if (Persistable.class.isAssignableFrom(entityType)) { 424 | return new PersistableEntityInformation(entityType); 425 | } 426 | return new ReflectionEntityInformation(entityType); 427 | } 428 | 429 | private void throwOnChangeAfterInitialization(String propertyName) { 430 | if (initialized) { 431 | throw new IllegalStateException( 432 | propertyName + " should not be changed after initialization"); 433 | } 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Spring Data JDBC generic DAO implementation 2 | :source-language: java 3 | // Project meta 4 | :name: spring-data-jdbc-repository 5 | :version: 0.5.0 6 | :group-id: cz.jirutka.spring 7 | :artifact-id: {name} 8 | :gh-name: jirutka/{name} 9 | :gh-branch: master 10 | :appveyor-id: n3x2wog0vys5bgl0 11 | :codacy-id: f44c7cac230b469793750a6899e286d6 12 | // URIs 13 | :src-base: link:src/main/java/cz/jirutka/spring/data/jdbc 14 | :src-test-base: link:src/test/groovy/cz/jirutka/spring/data/jdbc 15 | :src-fixtures-base: link:src/test/java/cz/jirutka/spring/data/jdbc/fixtures 16 | :spring-jdoc-uri: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework 17 | :spring-data-jdoc-uri: https://docs.spring.io/spring-data/data-commons/docs/current/api/org/springframework/data 18 | :javase-jdoc-uri: https://docs.oracle.com/javase/7/docs/api/java 19 | 20 | ifdef::env-github[] 21 | image:https://travis-ci.org/{gh-name}.svg?branch={gh-branch}["Build Status", link="https://travis-ci.org/{gh-name}"] 22 | image:https://ci.appveyor.com/api/projects/status/{appveyor-id}/branch/{gh-branch}?svg=true["Build status (Windows)", link="https://ci.appveyor.com/project/{gh-name}/branch/{gh-branch}"] 23 | image:https://api.codacy.com/project/badge/grade/{codacy-id}["Codacy code quality", link="https://www.codacy.com/app/{gh-name}"] 24 | image:https://maven-badges.herokuapp.com/maven-central/{group-id}/{artifact-id}/badge.svg[Maven Central, link="https://maven-badges.herokuapp.com/maven-central/{group-id}/{artifact-id}"] 25 | endif::env-github[] 26 | 27 | 28 | The purpose of this project is to provide generic, lightweight and easy to use DAO implementation for relational databases based on {spring-jdoc-uri}/jdbc/core/JdbcTemplate.html[JdbcTemplate] from https://projects.spring.io/spring-framework[Spring framework], compatible with Spring Data umbrella of projects. 29 | It’s intended for small applications where JPA or even MyBatis is an overkill. 30 | 31 | This project is a fork of https://github.com/nurkiewicz/spring-data-jdbc-repository[nurkiewicz/spring-data-jdbc-repository]. 32 | See link:CHANGELOG.adoc[CHANGELOG] for a list of changes. 33 | 34 | 35 | == Design objectives 36 | 37 | * Lightweight, fast and simple; only a handful of classes, *no XML, annotations or reflection.* 38 | * *Not a full-blown ORM*, just a simple Data Mapper. 39 | * No relationship handling, lazy loading, dirty checking, multi-level caching… just a https://en.wikipedia.org/wiki/Don't_repeat_yourself[DRY] https://en.wikipedia.org/wiki/Create,_read,_update_and_delete[CRUD]. 40 | * Standard repository interface from https://projects.spring.io/spring-data[Spring Data]; allow easier migration to other Spring Data implementations (e.g. JPA, Couchbase…).footnote:[Since your code will rely only on interfaces from Spring Data Commons umbrella project you are free to switch from `JdbcRepository` implementation (from this project) to `JpaRepository`, `GemfireRepository`, `GraphRepository`… see https://projects.spring.io/spring-data[Spring Data webpage]. They all implement the same common API. Of course don’t expect that switching from JDBC to e.g. JPA will be as simple as switching imported JAR dependencies – but at least you minimize the impact by using same DAO API.] 41 | * Minimalistic support for database dialect differences (e.g. transparent paging of results). 42 | 43 | 44 | == Features 45 | 46 | Each DAO provides built-in support for: 47 | 48 | * all methods defined in {spring-data-jdoc-uri}/repository/PagingAndSortingRepository.html[`PagingAndSortingRepository`] and {spring-data-jdoc-uri}/repository/CrudRepository.html[`CrudRepository`] (see <>), 49 | * mapping to/from domain objects through {spring-jdoc-uri}/jdbc/core/RowMapper.html[`RowMapper`] abstraction, 50 | * generated and user-defined primary keys, 51 | * compound (multi-column) primary keys, 52 | * paging (requesting subset of results) and sorting over several columns (see <>), 53 | * optional support for _many-to-one_ relationships, 54 | * immutable domain objects, 55 | * all major SQL databases (see list of <>). 56 | 57 | 58 | === Repository API 59 | 60 | API is compatible with Spring Data {spring-data-jdoc-uri}/repository/PagingAndSortingRepository.html[`PagingAndSortingRepository`] abstraction, i.e. all these methods are implemented for you: 61 | 62 | `long count()`:: 63 | Returns the number of entities available. 64 | `void delete(ID id)`:: 65 | Deletes the entity with the given id. 66 | `void delete(Iterable entities)`:: 67 | Deletes the given entities. 68 | `void delete(T entity)`:: 69 | Deletes the given entity. 70 | `void deleteAll()`:: 71 | Deletes all entities managed by the repository. 72 | `boolean exists(ID id)`:: 73 | Returns whether an entity with the given id exists. 74 | `Iterable findAll()`:: 75 | Returns all instances of the type. 76 | `Iterable findAll(Iterable ids)`:: 77 | Returns all instances of the type with the given IDs. 78 | `Page findAll(Pageable pageable)`:: 79 | Returns a Page of entities meeting the paging restriction provided in the Pageable object. 80 | `Iterable findAll(Sort sort)`:: 81 | Returns all entities sorted by the given options. 82 | `T findOne(ID id)`:: 83 | Retrieves an entity by its id. 84 | ` Iterable save(Iterable entities)`:: 85 | Saves all given entities. 86 | ` S save(S entity)`:: 87 | Saves the given entity. 88 | 89 | 90 | === Paging 91 | 92 | `Pageable` and `Sort` parameters are also fully supported, which means you get *paging and sorting by arbitrary properties for free*. 93 | For example, say you have `UserRepository` extending `PagingAndSortingRepository` interface (implemented for you by the library) and you request 5th page of `USERS` table, 10 per page, after applying some sorting: 94 | 95 | [source] 96 | ---- 97 | Page page = userRepository.findAll( 98 | new PageRequest(5, 10, new Sort( 99 | new Order(DESC, "reputation"), 100 | new Order(ASC, "user_name") 101 | )) 102 | ); 103 | ---- 104 | 105 | Spring Data JDBC repository will translate this call into (PostgreSQL syntax): 106 | 107 | [source, sql] 108 | ---- 109 | SELECT * 110 | FROM users 111 | ORDER BY reputation DESC, user_name ASC 112 | LIMIT 50 OFFSET 10 113 | ---- 114 | 115 | …or even (Derby/Oracle syntax): 116 | 117 | [source, sql] 118 | ---- 119 | SELECT * FROM ( 120 | SELECT ROW_NUMBER() OVER () AS ROW_NUM, t.* 121 | FROM ( 122 | SELECT * 123 | FROM users 124 | ORDER BY reputation DESC, user_name ASC 125 | ) AS t 126 | ) AS a 127 | WHERE ROW_NUM BETWEEN 51 AND 60 128 | ---- 129 | 130 | No matter which database you use, you’ll get `Page` object in return (you still have to provide `RowMapper` yourself to translate from {javase-jdoc-uri}/sql/ResultSet.html[`ResultSet`] to a domain object). 131 | If you don’t know Spring Data project yet, {spring-data-jdoc-uri}/domain/Page.html[`Page`] is a wonderful abstraction, not only encapsulating `List`, but also providing metadata such as total number of records, on which page we currently are etc. 132 | 133 | 134 | === Supported databases 135 | 136 | * http://www.postgresql.org[PostgreSQL] 137 | * https://db.apache.org/derby[Apache Derby] 138 | * http://www.h2database.com[H2] 139 | * http://hsqldb.org[HSQLDB] 140 | * https://mariadb.org[MariaDB] 141 | * https://www.microsoft.com/en-us/server-cloud/products/sql-server[MS SQL Server] 2008+ 142 | * https://www.mysql.com[MySQL] 143 | * https://www.oracle.com/database[Oracle Database] 11g+ (9i+ should work too) 144 | * …and most likely many others 145 | 146 | All of these databases are continuously tested on AppVeyor (MS SQL) and Travis CI (all others). 147 | The test suite consists of over 60 distinct tests. 148 | 149 | 150 | == Getting started 151 | 152 | For more examples and working code don’t forget to examine {src-test-base}[project tests]. 153 | 154 | In order to start your project must have `DataSource` bean present and transaction management enabled. 155 | Here is a minimal configuration for PostgreSQL with https://github.com/brettwooldridge/HikariCP[HikariCP] connection pool: 156 | 157 | [source] 158 | ---- 159 | @EnableTransactionManagement 160 | @Configuration 161 | public class MinimalConfig { 162 | 163 | @Bean 164 | public PlatformTransactionManager transactionManager() { 165 | return new DataSourceTransactionManager(dataSource()); 166 | } 167 | 168 | @Bean(destroyMethod = "shutdown") 169 | public DataSource dataSource() { 170 | Properties props = new Properties(); 171 | props.setProperty("dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource"); 172 | props.setProperty("dataSource.user", "test"); 173 | props.setProperty("dataSource.password", "test"); 174 | props.setProperty("dataSource.databaseName", "mydb"); 175 | 176 | return new HikariDataSource(new HikariConfig(props)); 177 | } 178 | } 179 | ---- 180 | 181 | === Entity with auto-generated key 182 | 183 | Say you have a following database table with auto-generated key (PostgreSQL syntax): 184 | 185 | [source, sql] 186 | ---- 187 | CREATE TABLE comments ( 188 | id serial PRIMARY KEY, 189 | user_name text, 190 | contents text, 191 | created_time timestamp NOT NULL 192 | ); 193 | ---- 194 | 195 | First you need to create domain object `User` mapping to that table (just like in any other ORM or Data Mapper): 196 | 197 | [source] 198 | ---- 199 | public class Comment implements Persistable { 200 | 201 | private Integer id; 202 | private String userName; 203 | private String contents; 204 | private Date createdTime; 205 | 206 | @Override 207 | public Integer getId() { 208 | return id; 209 | } 210 | 211 | @Override 212 | public boolean isNew() { 213 | return id == null; 214 | } 215 | 216 | // constructors / getters / setters / ... 217 | } 218 | ---- 219 | 220 | Apart from standard Java boilerplate you should notice implementing {spring-data-jdoc-uri}/domain/Persistable.html[`Persistable`] where `Integer` is the type of primary key. 221 | `Persistable` is an interface coming from Spring Data project and it’s the only requirement we place on your domain object. 222 | 223 | Finally we are ready to create our {src-fixtures-base}/CommentRepository.java[`CommentRepository`] DAO: 224 | 225 | [source] 226 | ---- 227 | @Repository 228 | public class CommentRepository extends JdbcRepository { 229 | 230 | public static final RowMapper ROW_MAPPER = // see below 231 | 232 | public static final RowUnmapper ROW_UNMAPPER = // see below 233 | 234 | public CommentRepository() { 235 | super(ROW_MAPPER, ROW_UNMAPPER, "comments"); 236 | } 237 | 238 | @Override 239 | protected S postCreate(S entity, Number generatedId) { 240 | entity.setId(generatedId.intValue()); 241 | return entity; 242 | } 243 | } 244 | ---- 245 | 246 | First of all we use {spring-jdoc-uri}/stereotype/Repository.html[`@Repository`] annotation to mark DAO bean. 247 | It enables persistence exception translation. 248 | Also such annotated beans are discovered by classpath scanning. 249 | 250 | As you can see we extend `JdbcRepository` which is the central class of this library, providing implementations of all `PagingAndSortingRepository` methods. 251 | Its constructor has three required dependencies: `RowMapper`, {src-base}/RowUnmapper.java[`RowUnmapper`] and table name. 252 | You may also provide ID column name, otherwise default `id` is used. 253 | 254 | If you ever used `JdbcTemplate` from Spring, you should be familiar with {spring-jdoc-uri}/jdbc/core/RowMapper.html[`RowMapper`] interface. 255 | We need to somehow extract columns from `ResultSet` into an object. 256 | After all we don’t want to work with raw JDBC results. 257 | It’s quite straightforward: 258 | 259 | [source] 260 | ---- 261 | public static final RowMapper ROW_MAPPER = new RowMapper() { 262 | 263 | public Comment mapRow(ResultSet rs, int rowNum) throws SQLException { 264 | return new Comment( 265 | rs.getInt("id"), 266 | rs.getString("user_name"), 267 | rs.getString("contents"), 268 | rs.getTimestamp("created_time") 269 | ); 270 | } 271 | }; 272 | ---- 273 | 274 | `RowUnmapper` comes from this library and it’s essentially the opposite of `RowMapper`: takes an object and turns it into a `Map`. 275 | This map is later used by the library to construct SQL `CREATE`/`UPDATE` queries: 276 | 277 | [source] 278 | ---- 279 | private static final RowUnmapper ROW_UNMAPPER = new RowUnmapper() { 280 | 281 | public Map mapColumns(Comment comment) { 282 | Map row = new LinkedHashMap(); 283 | row.put("id", comment.getId()); 284 | row.put("user_name", comment.getUserName()); 285 | row.put("contents", comment.getContents()); 286 | row.put("created_time", new Timestamp(comment.getCreatedTime().getTime())); 287 | return row; 288 | } 289 | }; 290 | ---- 291 | 292 | If you never update your database table (just reading some reference data inserted elsewhere) you may skip `RowUnmapper` parameter or use {src-base}/MissingRowUnmapper.java[`MissingRowUnmapper`]. 293 | 294 | Last piece of the puzzle is the `postCreate()` callback method which is called after an object was inserted. 295 | You can use it to retrieve generated primary key and update your domain object (or return new one if your domain objects are immutable). 296 | If you don’t need it, just don’t override `postCreate()`. 297 | 298 | Check out {src-test-base}/JdbcRepositoryGeneratedKeyIT.java[`JdbcRepositoryGeneratedKeyIT`] for a working code based on this example. 299 | 300 | **** 301 | By now you might have a feeling that, compared to JPA or Hibernate, there is quite a lot of manual work. 302 | However various JPA implementations and other ORM frameworks are notoriously known for introducing significant overhead and manifesting some learning curve. 303 | This tiny library intentionally leaves some responsibilities to the user in order to avoid complex mappings, reflection, annotations… all the implicitness that is not always desired. 304 | 305 | This project is not intending to replace mature and stable ORM frameworks. 306 | Instead it tries to fill in a niche between raw JDBC and ORM where simplicity and low overhead are key features. 307 | **** 308 | 309 | 310 | === Entity with manually assigned key 311 | 312 | In this example we’ll see how entities with user-defined primary keys are handled. 313 | Let’s start from database model: 314 | 315 | [source, sql] 316 | ---- 317 | CREATE TABLE users ( 318 | user_name text PRIMARY KEY, 319 | date_of_birth timestamp NOT NULL, 320 | enabled boolean NOT NULL 321 | ); 322 | ---- 323 | 324 | …and `User` domain model: 325 | 326 | [source] 327 | ---- 328 | public class User implements Persistable { 329 | 330 | private transient boolean persisted; 331 | 332 | private String userName; 333 | private Date dateOfBirth; 334 | private boolean enabled; 335 | 336 | @Override 337 | public String getId() { 338 | return userName; 339 | } 340 | 341 | @Override 342 | public boolean isNew() { 343 | return !persisted; 344 | } 345 | 346 | public void setPersisted(boolean persisted) { 347 | this.persisted = persisted; 348 | } 349 | 350 | // constructors / getters / setters / ... 351 | } 352 | ---- 353 | 354 | Notice that special `persisted` transient flag was added. 355 | Contract of {spring-data-jdoc-uri}/repository/CrudRepository.html#save(S)[`CrudRepository.save()`] from Spring Data project requires that an entity knows whether it was already saved or not (`isNew()`) method – there are no separate `create()` and `update()` methods. 356 | Implementing `isNew()` is simple for auto-generated keys (see `Comment` above) but in this case we need an extra transient field. 357 | If you hate this workaround and you only insert data and never update, you’ll get away with return `true` all the time from `isNew()`. 358 | 359 | And finally our DAO, {src-fixtures-base}/UserRepository.java[`UserRepository`] bean: 360 | 361 | [source] 362 | ---- 363 | @Repository 364 | public class UserRepository extends JdbcRepository { 365 | 366 | public static final RowMapper ROW_MAPPER = //... 367 | 368 | public static final RowUnmapper ROW_UNMAPPER = //... 369 | 370 | public UserRepository() { 371 | super(ROW_MAPPER, ROW_UNMAPPER, "USERS", "user_name"); 372 | } 373 | 374 | @Override 375 | protected S postUpdate(S entity) { 376 | entity.setPersisted(true); 377 | return entity; 378 | } 379 | 380 | @Override 381 | protected S postCreate(S entity, Number generatedId) { 382 | entity.setPersisted(true); 383 | return entity; 384 | } 385 | } 386 | ---- 387 | 388 | The `users` and `user_name` parameters designate table name and primary key column name. 389 | I’ll leave the details of mapper and unmapper (see {src-fixtures-base}/UserRepository.java[source code]). 390 | But please notice `postUpdate()` and `postCreate()` methods. 391 | They ensure that once object was persisted, `persisted` flag is set so that subsequent calls to `save()` will update existing entity rather than trying to reinsert it. 392 | 393 | Check out {src-test-base}/JdbcRepositoryManualKeyIT.java[`JdbcRepositoryManualKeyIT`] for a working code based on this example. 394 | 395 | 396 | === Compound primary key 397 | 398 | We also support compound primary keys (primary keys consisting of several columns). 399 | Take this table as an example: 400 | 401 | [source, sql] 402 | ---- 403 | CREATE TABLE boarding_pass ( 404 | flight_no varchar(8) NOT NULL, 405 | seq_no integer NOT NULL, 406 | passenger text, 407 | seat char(3), 408 | PRIMARY KEY (flight_no, seq_no) 409 | ); 410 | ---- 411 | 412 | I would like you to notice the type of primary key in `Persistable`: 413 | 414 | [source] 415 | ---- 416 | public class BoardingPass implements Persistable { 417 | 418 | private transient boolean persisted; 419 | 420 | private String flightNo; 421 | private int seqNo; 422 | private String passenger; 423 | private String seat; 424 | 425 | @Override 426 | public Object[] getId() { 427 | return pk(flightNo, seqNo); 428 | } 429 | 430 | @Override 431 | public boolean isNew() { 432 | return !persisted; 433 | } 434 | 435 | // constructors / getters / setters / ... 436 | } 437 | ---- 438 | 439 | Unfortunately library does not support small, immutable value classes encapsulating all ID values in one object (like JPA does with http://docs.oracle.com/javaee/6/api/javax/persistence/IdClass.html[`@IdClass`]), so you have to live with `Object[]` array. 440 | Defining DAO class is similar to what we’ve already seen: 441 | 442 | [source] 443 | ---- 444 | public class BoardingPassRepository extends JdbcRepository { 445 | 446 | public static final RowMapper ROW_MAPPER = //... 447 | 448 | public static final RowUnmapper UNMAPPER = //... 449 | 450 | public BoardingPassRepository() { 451 | super(MAPPER, UNMAPPER, 452 | new TableDescription("BOARDING_PASS", null, "flight_no", "seq_no")); 453 | } 454 | } 455 | ---- 456 | 457 | Two things to notice: we extend `JdbcRepository` and we provide two ID column names just as expected: `flight_no, seq_no`. 458 | We query such DAO by providing both `flight_no` and `seq_no` (necessarily in that order) values wrapped by `Object[]`: 459 | 460 | [source] 461 | BoardingPass pass = boardingPassRepository.findOne(new Object[]{"FOO-1022", 42}); 462 | 463 | No doubts, this is cumbersome in practice, so you may create a tiny utility method for it: 464 | 465 | [source] 466 | ---- 467 | public static Object[] pk(Object... idValues) { 468 | return idValues; 469 | } 470 | ---- 471 | 472 | …and then use it as: 473 | 474 | [source] 475 | BoardingPass foundFlight = boardingPassRepository.findOne(pk("FOO-1022", 42)); 476 | 477 | …or just use some more expressive JVM-based language as Groovy. ;) 478 | 479 | Check out link:src/test/java/cz/jirutka/spring/data/jdbc/JdbcRepositoryCompoundPkIT.java[`JdbcRepositoryCompoundPkIT`] for a working code based on this example. 480 | 481 | 482 | === Transactions 483 | 484 | This library is completely orthogonal to transaction management. 485 | Every method of each repository requires running transaction and it’s up to you to set it up. 486 | Typically you would place `@Transactional` on service layer (calling DAO beans). 487 | Please not that it’s generally not recommend to https://stackoverflow.com/questions/8993318[place @Transactional over every DAO bean]. 488 | 489 | 490 | === Caching 491 | 492 | This library does not provide any caching abstraction or support. 493 | However, adding `@Cacheable` layer on top of your DAOs or services using https://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html[caching abstraction in Spring] is quite straightforward. 494 | See also: http://nurkiewicz.blogspot.no/2013/01/cacheable-overhead-in-spring.html[_@Cacheable overhead in Spring_]. 495 | 496 | 497 | == How to get it? 498 | 499 | Released versions are available in The Central Repository. 500 | Just add this artifact to your project: 501 | 502 | ._Maven_ 503 | [source, xml, subs="verbatim, attributes"] 504 | ---- 505 | 506 | {group-id} 507 | {artifact-id} 508 | {version} 509 | 510 | ---- 511 | 512 | ._Gradle_ 513 | [source, groovy, subs="verbatim, attributes"] 514 | compile '{group-id}:{artifact-id}:{version}' 515 | 516 | However if you want to use the last snapshot version, you have to add the JFrog OSS repository: 517 | 518 | ._Maven_ 519 | [source, xml] 520 | ---- 521 | 522 | jfrog-oss-snapshot-local 523 | JFrog OSS repository for snapshots 524 | https://oss.jfrog.org/oss-snapshot-local 525 | 526 | true 527 | 528 | 529 | ---- 530 | 531 | ._Gradle_ 532 | [source, groovy] 533 | ---- 534 | repositories { 535 | maven { 536 | url 'https://oss.jfrog.org/oss-snapshot-local' 537 | } 538 | } 539 | ---- 540 | 541 | 542 | == Contributions 543 | 544 | …are always welcome. 545 | Don’t hesitate to submit a https://github.com/{gh-name}/issues[bug report] or a https://github.com/{gh-name}/pulls[pull requests]. 546 | 547 | When filling a bug report or submitting a new feature, please try including supporting test cases. 548 | 549 | 550 | == License 551 | 552 | This project is licensed under http://www.apache.org/licenses/LICENSE-2.0.html[Apache License 2.0]. 553 | --------------------------------------------------------------------------------