diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 91ff0bd9bc..7fa829620a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,10 +31,17 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: Maven version run: mvn --version # - name: Maven single test # run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false - name: Build with Maven - run: mvn -T 8 clean test + run: mvn -T 8 clean test -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/db2luw.yml b/.github/workflows/db2luw.yml index f4e6871eff..3af0c4e5b1 100644 --- a/.github/workflows/db2luw.yml +++ b/.github/workflows/db2luw.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: db2 - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/h2database.yml b/.github/workflows/h2database.yml index 935c0540ba..097c36de09 100644 --- a/.github/workflows/h2database.yml +++ b/.github/workflows/h2database.yml @@ -34,8 +34,15 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: Maven version run: mvn --version - name: H2Database - run: mvn -T 8 clean package + run: mvn -T 8 clean package -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/jdk-ea.yml b/.github/workflows/jdk-ea.yml index a9499ddf29..edf4e80f98 100644 --- a/.github/workflows/jdk-ea.yml +++ b/.github/workflows/jdk-ea.yml @@ -34,8 +34,15 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: Maven version run: mvn --version - name: Build with Maven - run: mvn -T 8 test + run: mvn -T 8 test -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mariadb.yml b/.github/workflows/mariadb.yml index 990512b7d9..7cf79006b2 100644 --- a/.github/workflows/mariadb.yml +++ b/.github/workflows/mariadb.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: mariadb 10.6 - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/multi-db-platform.yml b/.github/workflows/multi-db-platform.yml index aa3a9fce58..b43cd00c0b 100644 --- a/.github/workflows/multi-db-platform.yml +++ b/.github/workflows/multi-db-platform.yml @@ -31,18 +31,35 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: h2database - run: mvn clean test + run: mvn clean test -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: postgres - run: mvn clean test -Dprops.file=testconfig/ebean-postgres.properties + run: mvn clean test -Dprops.file=testconfig/ebean-postgres.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: mysql - run: mvn clean test -Dprops.file=testconfig/ebean-mysql.properties + run: mvn clean test -Dprops.file=testconfig/ebean-mysql.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: mariadb - run: mvn clean test -Dprops.file=testconfig/ebean-mariadb.properties + run: mvn clean test -Dprops.file=testconfig/ebean-mariadb.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: yugabyte - run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties + run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: sqlserver - run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver17.properties + run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver17.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # - name: sqlserver19 # run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver19.properties # - name: db2 diff --git a/.github/workflows/multi-jdk-build.yml b/.github/workflows/multi-jdk-build.yml index a7f2b3284f..b1e33af206 100644 --- a/.github/workflows/multi-jdk-build.yml +++ b/.github/workflows/multi-jdk-build.yml @@ -34,6 +34,13 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: Build with Maven - run: mvn package + run: mvn package -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml index 405fbf2704..4afc80e78a 100644 --- a/.github/workflows/mysql.yml +++ b/.github/workflows/mysql.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: mysql - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/oracle.yml b/.github/workflows/oracle.yml index 5144651d62..168d166553 100644 --- a/.github/workflows/oracle.yml +++ b/.github/workflows/oracle.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: oracle - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index ae18b96802..d6500c2c27 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: postgres - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sqlserver-2019.yml b/.github/workflows/sqlserver-2019.yml index 145b39c8fa..0cee846227 100644 --- a/.github/workflows/sqlserver-2019.yml +++ b/.github/workflows/sqlserver-2019.yml @@ -31,5 +31,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: sqlserver 2019 latest - run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver19.properties + run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver19.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sqlserver.yml b/.github/workflows/sqlserver.yml index 25d245b564..2b9448d728 100644 --- a/.github/workflows/sqlserver.yml +++ b/.github/workflows/sqlserver.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: sqlserver 2017 - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver17.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver17.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/yugabyte.yml b/.github/workflows/yugabyte.yml index ba5ef3a517..bd64e1b868 100644 --- a/.github/workflows/yugabyte.yml +++ b/.github/workflows/yugabyte.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: yugabyte - run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties + run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 37cb0c28d9..ae246ab3f7 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,26 @@ -[![Build](https://github.com/ebean-orm/ebean/actions/workflows/build.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/build.yml) +[![Build](https://github.com/FOCONIS/ebean/actions/workflows/build.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/build.yml) [![Maven Central : ebean](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ebean-orm/ebean/blob/master/LICENSE) -[![Multi-JDK Build](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/FOCONIS/ebean/blob/master/LICENSE) +[![Multi-JDK Build](https://github.com/FOCONIS/ebean/actions/workflows/multi-jdk-build.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/multi-jdk-build.yml) ##### Build with database platforms -[![H2Database](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml) -[![Postgres](https://github.com/ebean-orm/ebean/actions/workflows/postgres.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/postgres.yml) -[![MySql](https://github.com/ebean-orm/ebean/actions/workflows/mysql.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/mysql.yml) -[![MariaDB](https://github.com/ebean-orm/ebean/actions/workflows/mariadb.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/mariadb.yml) -[![Oracle](https://github.com/ebean-orm/ebean/actions/workflows/oracle.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/oracle.yml) -[![SqlServer](https://github.com/ebean-orm/ebean/actions/workflows/sqlserver.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/sqlserver.yml) -[![DB2 LUW](https://github.com/ebean-orm/ebean/actions/workflows/db2luw.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/db2luw.yml) -[![Yugabyte](https://github.com/ebean-orm/ebean/actions/workflows/yugabyte.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/yugabyte.yml) +[![H2Database](https://github.com/FOCONIS/ebean/actions/workflows/h2database.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/h2database.yml) +[![Postgres](https://github.com/FOCONIS/ebean/actions/workflows/postgres.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/postgres.yml) +[![MySql](https://github.com/FOCONIS/ebean/actions/workflows/mysql.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/mysql.yml) +[![MariaDB](https://github.com/FOCONIS/ebean/actions/workflows/mariadb.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/mariadb.yml) +[![Oracle](https://github.com/FOCONIS/ebean/actions/workflows/oracle.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/oracle.yml) +[![SqlServer](https://github.com/FOCONIS/ebean/actions/workflows/sqlserver.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/sqlserver.yml) +[![DB2 LUW](https://github.com/FOCONIS/ebean/actions/workflows/db2luw.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/db2luw.yml) +[![Yugabyte](https://github.com/FOCONIS/ebean/actions/workflows/yugabyte.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/yugabyte.yml) + ##### Build with Java Early Access versions -[![ebean EA](https://github.com/ebean-orm/ebean/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/jdk-ea.yml) -[![datasource EA](https://github.com/ebean-orm/ebean-datasource/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-datasource/actions/workflows/jdk-ea.yml) -[![migration EA](https://github.com/ebean-orm/ebean-migration/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-migration/actions/workflows/jdk-ea.yml) -[![test-docker EA](https://github.com/ebean-orm/ebean-test-docker/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-test-docker/actions/workflows/jdk-ea.yml) -[![ebean-agent EA](https://github.com/ebean-orm/ebean-agent/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-agent/actions/workflows/jdk-ea.yml) +[![ebean EA](https://github.com/FOCONIS/ebean/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/jdk-ea.yml) +[![datasource EA](https://github.com/FOCONIS/ebean-datasource/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean-datasource/actions/workflows/jdk-ea.yml) +[![migration EA](https://github.com/FOCONIS/ebean-migration/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean-migration/actions/workflows/jdk-ea.yml) +[![test-docker EA](https://github.com/FOCONIS/ebean-test-docker/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean-test-docker/actions/workflows/jdk-ea.yml) +[![ebean-agent EA](https://github.com/FOCONIS/ebean-agent/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean-agent/actions/workflows/jdk-ea.yml) ---------------------- diff --git a/composites/ebean-clickhouse/pom.xml b/composites/ebean-clickhouse/pom.xml index 1cfd90d485..e4a967751c 100644 --- a/composites/ebean-clickhouse/pom.xml +++ b/composites/ebean-clickhouse/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-clickhouse @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-clickhouse - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-cockroach/pom.xml b/composites/ebean-cockroach/pom.xml index f5d2e941f4..2bbd87a740 100644 --- a/composites/ebean-cockroach/pom.xml +++ b/composites/ebean-cockroach/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-cockroach @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-postgres - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-db2/pom.xml b/composites/ebean-db2/pom.xml index 6b9a6b862d..4f7c3d6d3f 100644 --- a/composites/ebean-db2/pom.xml +++ b/composites/ebean-db2/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-db2 @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-db2 - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-h2/pom.xml b/composites/ebean-h2/pom.xml index 73628e4bca..9ec87d943f 100644 --- a/composites/ebean-h2/pom.xml +++ b/composites/ebean-h2/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-h2 @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-h2 - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-hana/pom.xml b/composites/ebean-hana/pom.xml index 53431e1250..2d58890a24 100644 --- a/composites/ebean-hana/pom.xml +++ b/composites/ebean-hana/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-hana @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-hana - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-mariadb/pom.xml b/composites/ebean-mariadb/pom.xml index 75ec5fcbfa..f1d45f79f3 100644 --- a/composites/ebean-mariadb/pom.xml +++ b/composites/ebean-mariadb/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-mariadb @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-mariadb - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-mysql/pom.xml b/composites/ebean-mysql/pom.xml index 17d384133c..a6f19925b4 100644 --- a/composites/ebean-mysql/pom.xml +++ b/composites/ebean-mysql/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-mysql @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-mysql - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-nuodb/pom.xml b/composites/ebean-nuodb/pom.xml index c3adfe0fcc..c2a9d9f652 100644 --- a/composites/ebean-nuodb/pom.xml +++ b/composites/ebean-nuodb/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-nuodb @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-nuodb - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-oracle/pom.xml b/composites/ebean-oracle/pom.xml index 9020eb70d7..1f74f18bf9 100644 --- a/composites/ebean-oracle/pom.xml +++ b/composites/ebean-oracle/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-oracle @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-oracle - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-postgres/pom.xml b/composites/ebean-postgres/pom.xml index 41299ed280..63f1b13288 100644 --- a/composites/ebean-postgres/pom.xml +++ b/composites/ebean-postgres/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-postgres @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-postgres - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-sqlite/pom.xml b/composites/ebean-sqlite/pom.xml index 3a30ef76d0..f1f8557496 100644 --- a/composites/ebean-sqlite/pom.xml +++ b/composites/ebean-sqlite/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-sqlite @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-sqlite - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-sqlserver/pom.xml b/composites/ebean-sqlserver/pom.xml index c9afc81550..025fc5d2c3 100644 --- a/composites/ebean-sqlserver/pom.xml +++ b/composites/ebean-sqlserver/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-sqlserver @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-sqlserver - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean-yugabyte/pom.xml b/composites/ebean-yugabyte/pom.xml index b9f53060bd..d79c5e72e6 100644 --- a/composites/ebean-yugabyte/pom.xml +++ b/composites/ebean-yugabyte/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-yugabyte @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-postgres - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/ebean/pom.xml b/composites/ebean/pom.xml index 1c3046a181..9df5ba9512 100644 --- a/composites/ebean/pom.xml +++ b/composites/ebean/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean (all platforms) @@ -16,31 +16,31 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-joda-time - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-jackson-jsonnode - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-jackson-mapper - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -53,13 +53,13 @@ io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-platform-all - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/composites/pom.xml b/composites/pom.xml index 91386205e6..72f81e7e71 100644 --- a/composites/pom.xml +++ b/composites/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT composites diff --git a/ebean-api/pom.xml b/ebean-api/pom.xml index 8dc715f1a0..b9db71cf52 100644 --- a/ebean-api/pom.xml +++ b/ebean-api/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean api diff --git a/ebean-api/src/main/java/io/ebean/BeanMergeOptions.java b/ebean-api/src/main/java/io/ebean/BeanMergeOptions.java new file mode 100644 index 0000000000..daa8dd2657 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/BeanMergeOptions.java @@ -0,0 +1,126 @@ +package io.ebean; + +import io.ebean.bean.PersistenceContext; +import io.ebean.plugin.Property; + +/** + * Merge options, when merging two beans. You can fine tune, how the merge should happen. + * For example you can exclude some special properties or write your custom merge handler. + * + * @author Roland Praml, FOCONIS AG + */ +public class BeanMergeOptions { + + /** + * Interface to write your own merge handler. + * + * @param + */ + @FunctionalInterface + public interface MergeHandler { + /** + * The new bean and the existing is passed. Together with property + * and path, you can decide, if you want to continue with merge or not. + */ + boolean mergeBeans(T bean, T existing, Property property, String path); + + } + + private PersistenceContext persistenceContext; + + private MergeHandler mergeHandler; + + private boolean mergeId = true; + + private boolean mergeVersion = false; + + private boolean clearCollections = true; + + private boolean addExistingToPersistenceContext = true; + + /** + * Return the persistence context, that is used during merge. + * If no one is specified, the persistence context of the bean will be used + */ + public PersistenceContext getPersistenceContext() { + return persistenceContext; + } + + /** + * Sets the persistence context, that is used during merge. + * * If no one is specified, the persistence context of the bean will be used + */ + public void setPersistenceContext(PersistenceContext persistenceContext) { + this.persistenceContext = persistenceContext; + } + + /** + * Returns the merge handler, if you want to do special handling for some properties. + */ + public MergeHandler getMergeHandler() { + return mergeHandler; + } + + /** + * Sets the merge handler, if you want to do special handling for some properties. + */ + public void setMergeHandler(MergeHandler mergeHandler) { + this.mergeHandler = mergeHandler; + } + + /** + * Returns if we should merge the ID property (default=true). + */ + public boolean isMergeId() { + return mergeId; + } + + /** + * Should we merge the ID property (default=true). + */ + public void setMergeId(boolean mergeId) { + this.mergeId = mergeId; + } + + /** + * Returns if we should merge the version property (default=false). + */ + public boolean isMergeVersion() { + return mergeVersion; + } + + /** + * Should we merge the version property (default=false). + */ + public void setMergeVersion(boolean mergeVersion) { + this.mergeVersion = mergeVersion; + } + + /** + * Returns if we should clear/replace beanCollections (default=true). + */ + public boolean isClearCollections() { + return clearCollections; + } + + /** + * Should we clear/replace beanCollections (default=true). + */ + public void setClearCollections(boolean clearCollections) { + this.clearCollections = clearCollections; + } + + /** + * Returns if we should add existing beans to the persistenceContext (default=true). + */ + public boolean isAddExistingToPersistenceContext() { + return addExistingToPersistenceContext; + } + + /** + * Should we add existing beans to the persistenceContext (default=true). + */ + public void setAddExistingToPersistenceContext(boolean addExistingToPersistenceContext) { + this.addExistingToPersistenceContext = addExistingToPersistenceContext; + } +} diff --git a/ebean-api/src/main/java/io/ebean/DB.java b/ebean-api/src/main/java/io/ebean/DB.java index f20bcb740c..4109291fd0 100644 --- a/ebean-api/src/main/java/io/ebean/DB.java +++ b/ebean-api/src/main/java/io/ebean/DB.java @@ -501,8 +501,8 @@ public static Set checkUniqueness(Object bean) { /** * Same as {@link #checkUniqueness(Object)} but with given transaction. */ - public static Set checkUniqueness(Object bean, Transaction transaction) { - return getDefault().checkUniqueness(bean, transaction); + public static Set checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) { + return getDefault().checkUniqueness(bean, transaction, useQueryCache, skipClean); } /** diff --git a/ebean-api/src/main/java/io/ebean/Database.java b/ebean-api/src/main/java/io/ebean/Database.java index 6dd0ab3cfa..adc2224d21 100644 --- a/ebean-api/src/main/java/io/ebean/Database.java +++ b/ebean-api/src/main/java/io/ebean/Database.java @@ -35,7 +35,7 @@ *
The 'default' Database
*

* One Database can be designated as the 'default' or 'primary' Database - * (see {@link DatabaseConfig#setDefaultServer(boolean)}. Many methods on DB + * (see {@link DatabaseConfig#setDefaultServer(boolean)}). Many methods on DB * such as {@link DB#find(Class)} etc are actually just a convenient way to * call methods on the 'default/primary' Database. * @@ -166,7 +166,7 @@ public interface Database { /** * Return the BeanState for a given entity bean. *

- * This will return null if the bean is not an enhanced entity bean. + * This will throw an IllegalArgumentException if the bean is not an enhanced entity bean. */ BeanState beanState(Object bean); @@ -1110,7 +1110,7 @@ public interface Database { /** * Same as {@link #checkUniqueness(Object)}. but with given transaction. */ - Set checkUniqueness(Object bean, Transaction transaction); + Set checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean); /** * Marks the entity bean as dirty. @@ -1195,6 +1195,12 @@ public interface Database { */ void merge(Object bean, MergeOptions options, Transaction transaction); + /** + * Merges two beans (without saving them). All modified properties from bean are copied to existing. + * Returns existing bean. If null is passed, a new instance of bean is retuned. + */ + T mergeBeans(T bean, T existing, BeanMergeOptions options); + /** * Insert the bean. *

@@ -1509,4 +1515,8 @@ public interface Database { */ void truncate(Class... beanTypes); + /** + * RunDdl manually. This can be used if 'db.ddl.run=false' is set and you plan to run DDL manually. + */ + void runDdl(); } diff --git a/ebean-api/src/main/java/io/ebean/EbeanVersion.java b/ebean-api/src/main/java/io/ebean/EbeanVersion.java index aeb855efce..8c543e3fe4 100644 --- a/ebean-api/src/main/java/io/ebean/EbeanVersion.java +++ b/ebean-api/src/main/java/io/ebean/EbeanVersion.java @@ -22,8 +22,8 @@ public final class EbeanVersion { /** * Maintain the minimum ebean-agent version manually based on required ebean-agent bug fixes. */ - private static final int MIN_AGENT_MAJOR_VERSION = 12; - private static final int MIN_AGENT_MINOR_VERSION = 12; + private static final int MIN_AGENT_MAJOR_VERSION = 13; + private static final int MIN_AGENT_MINOR_VERSION = 10; private static String version = "unknown"; static { diff --git a/ebean-api/src/main/java/io/ebean/ExpressionList.java b/ebean-api/src/main/java/io/ebean/ExpressionList.java index 4d0a2fc183..29040fcbae 100644 --- a/ebean-api/src/main/java/io/ebean/ExpressionList.java +++ b/ebean-api/src/main/java/io/ebean/ExpressionList.java @@ -404,6 +404,7 @@ public interface ExpressionList { * * } */ + @Nullable default A findSingleAttribute() { List list = findSingleAttributeList(); return !list.isEmpty() ? list.get(0) : null; diff --git a/ebean-api/src/main/java/io/ebean/Transaction.java b/ebean-api/src/main/java/io/ebean/Transaction.java index 065396a925..21ce076a8f 100644 --- a/ebean-api/src/main/java/io/ebean/Transaction.java +++ b/ebean-api/src/main/java/io/ebean/Transaction.java @@ -260,6 +260,12 @@ static Transaction current() { */ void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties); + /** + * If set to false (default is true) generated propertes are only set, if it is the version property or have a null value. + * This may be useful in backup & restore scenarios, if you want set WhenCreated/WhenModified. + */ + void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties); + /** * Set if the L2 cache should be skipped for "find by id" and "find by natural key" queries. *

diff --git a/ebean-api/src/main/java/io/ebean/annotation/ext/IntersectionFactory.java b/ebean-api/src/main/java/io/ebean/annotation/ext/IntersectionFactory.java new file mode 100644 index 0000000000..5749549fb9 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/annotation/ext/IntersectionFactory.java @@ -0,0 +1,29 @@ +package io.ebean.annotation.ext; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation to define a factory for an intersection model. This class MUST have a constructor or factory method with two parameters that accepts parent and property type. + * @author Roland Praml, FOCONIS AG + */ +@Documented +@Target({ FIELD, TYPE }) +@Retention(RUNTIME) +public @interface IntersectionFactory { + + /** + * The intersection model class. + */ + Class value(); + + /** + * An optional factory method. + */ + String factoryMethod() default ""; +} diff --git a/ebean-api/src/main/java/io/ebean/annotation/ext/package.html b/ebean-api/src/main/java/io/ebean/annotation/ext/package.html new file mode 100644 index 0000000000..9505c002ca --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/annotation/ext/package.html @@ -0,0 +1,8 @@ + + + Ebean Annotations + + +This classes will be moved later to the ebean-annotation module. + + diff --git a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java index 1161476ddd..dcb002266f 100644 --- a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java +++ b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java @@ -162,7 +162,17 @@ enum ModifyListenMode { *

* For maps this returns the entrySet as we need the keys of the map. */ - Collection actualEntries(); + Collection actualEntries(boolean load); + + default Collection actualEntries() { + return actualEntries(false); + } + /** + * Returns entries, that were lazily added at the end of the list. Might be null. + */ + default Collection getLazyAddedEntries(boolean reset) { + return null; + } /** * return true if there are real rows held. Return false is this is using @@ -240,4 +250,9 @@ enum ModifyListenMode { * Return a shallow copy of this collection that is modifiable. */ BeanCollection shallowCopy(); + + /** + * Clears the underlying collection. + */ + void clear(); } diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java index 53bfc9ba49..1a70a68571 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java @@ -41,6 +41,13 @@ default Object _ebean_newInstanceReadOnly() { throw new NotEnhancedException(); } + /** + * Creates a new instance and uses the provided intercept. (For EntityExtension) + */ + default Object _ebean_newExtendedInstance(int offset, EntityBean base) { + throw new NotEnhancedException(); + } + /** * Generated method that sets the loaded state on all the embedded beans on * this entity bean by using EntityBeanIntercept.setEmbeddedLoaded(Object o); @@ -120,4 +127,19 @@ default Object _ebean_getFieldIntercept(int fieldIndex) { default void toString(ToStringBuilder builder) { throw new NotEnhancedException(); } + + /** + * Returns the ExtensionAccessors, this is always NONE for non extendable beans. + */ + default ExtensionAccessors _ebean_getExtensionAccessors() { + return ExtensionAccessors.NONE; + } + + /** + * Returns the extension bean for an accessor. This will throw NotEnhancedException for non extendable beans. + * (It is not intended to call this method here) + */ + default EntityBean _ebean_getExtension(ExtensionAccessor accessor) { + throw new NotEnhancedException(); // not an extendableBean + } } diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index 1127c02b25..120068ad34 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -552,4 +552,24 @@ public interface EntityBeanIntercept extends Serializable { * Update the 'next' mutable info returning the content that was obtained via dirty detection. */ String mutableNext(int propertyIndex); + + /** + * Returns the value of the property. Can also return virtual properties. + */ + Object value(int propertyIndex); + + /** + * Returns the value of the property with intercept access. Can also return virtual properties. + */ + Object valueIntercept(int propertyIndex); + + /** + * Writes the value to the property. Can also write virtual properties. + */ + void setValue(int propertyIndex, Object value); + + /** + * Writes the value to the property with intercept access. Can also write virtual properties. + */ + void setValueIntercept(int propertyIndex, Object value); } diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java new file mode 100644 index 0000000000..9efdd8ee07 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java @@ -0,0 +1,562 @@ +package io.ebean.bean; + +import io.ebean.ValuePair; + +import java.util.Map; +import java.util.Set; + +/** + * Intercept for classes annotated with @EntityExtension. The intercept will delegate all calls to the base intercept of the + * ExtendableBean and adds given offset to all field operations. + * + * @author Roland Praml, FOCONIS AG + */ +public class EntityExtensionIntercept implements EntityBeanIntercept { + private final EntityBeanIntercept base; + private final int offset; + + public EntityExtensionIntercept(Object ownerBean, int offset, EntityBean base) { + this.base = base._ebean_getIntercept(); + this.offset = offset; + } + + @Override + public EntityBean owner() { + return base.owner(); + } + + @Override + public PersistenceContext persistenceContext() { + return base.persistenceContext(); + } + + @Override + public void setPersistenceContext(PersistenceContext persistenceContext) { + base.setPersistenceContext(persistenceContext); + } + + @Override + public void setNodeUsageCollector(NodeUsageCollector usageCollector) { + base.setNodeUsageCollector(usageCollector); + } + + @Override + public Object ownerId() { + return base.ownerId(); + } + + @Override + public void setOwnerId(Object ownerId) { + base.setOwnerId(ownerId); + } + + @Override + public Object embeddedOwner() { + return base.embeddedOwner(); + } + + @Override + public int embeddedOwnerIndex() { + return base.embeddedOwnerIndex(); + } + + @Override + public void clearGetterCallback() { + base.clearGetterCallback(); + } + + @Override + public void registerGetterCallback(PreGetterCallback getterCallback) { + base.registerGetterCallback(getterCallback); + } + + @Override + public void setEmbeddedOwner(EntityBean parentBean, int embeddedOwnerIndex) { + base.setEmbeddedOwner(parentBean, embeddedOwnerIndex); + } + + @Override + public void setBeanLoader(BeanLoader beanLoader, PersistenceContext ctx) { + base.setBeanLoader(beanLoader, ctx); + } + + @Override + public void setBeanLoader(BeanLoader beanLoader) { + base.setBeanLoader(beanLoader); + } + + @Override + public boolean isFullyLoadedBean() { + return base.isFullyLoadedBean(); + } + + @Override + public void setFullyLoadedBean(boolean fullyLoadedBean) { + base.setFullyLoadedBean(fullyLoadedBean); + } + + @Override + public boolean isPartial() { + return base.isPartial(); + } + + @Override + public boolean isDirty() { + return base.isDirty(); + } + + @Override + public void setEmbeddedDirty(int embeddedProperty) { + base.setEmbeddedDirty(embeddedProperty + offset); + } + + @Override + public void setDirty(boolean dirty) { + base.setDirty(dirty); + } + + @Override + public boolean isNew() { + return base.isNew(); + } + + @Override + public boolean isNewOrDirty() { + return base.isNewOrDirty(); + } + + @Override + public boolean hasIdOnly(int idIndex) { + return base.hasIdOnly(idIndex + offset); + } + + @Override + public boolean isReference() { + return base.isReference(); + } + + @Override + public void setReference(int idPos) { + base.setReference(idPos + offset); + } + + @Override + public void setLoadedFromCache(boolean loadedFromCache) { + base.setLoadedFromCache(loadedFromCache); + } + + @Override + public boolean isLoadedFromCache() { + return base.isLoadedFromCache(); + } + + @Override + public boolean isReadOnly() { + return base.isReadOnly(); + } + + @Override + public void setReadOnly(boolean readOnly) { + base.setReadOnly(readOnly); + } + + @Override + public void setForceUpdate(boolean forceUpdate) { + base.setForceUpdate(forceUpdate); + } + + @Override + public boolean isUpdate() { + return base.isUpdate(); + } + + @Override + public boolean isLoaded() { + return base.isLoaded(); + } + + @Override + public void setNew() { + base.setNew(); + } + + @Override + public void setLoaded() { + base.setLoaded(); + } + + @Override + public void setLoadedLazy() { + base.setLoadedLazy(); + } + + @Override + public void setLazyLoadFailure(Object ownerId) { + base.setLazyLoadFailure(ownerId); + } + + @Override + public boolean isLazyLoadFailure() { + return base.isLazyLoadFailure(); + } + + @Override + public boolean isDisableLazyLoad() { + return base.isDisableLazyLoad(); + } + + @Override + public void setDisableLazyLoad(boolean disableLazyLoad) { + base.setDisableLazyLoad(disableLazyLoad); + } + + @Override + public void setEmbeddedLoaded(Object embeddedBean) { + base.setEmbeddedLoaded(embeddedBean); + } + + @Override + public boolean isEmbeddedNewOrDirty(Object embeddedBean) { + return base.isEmbeddedNewOrDirty(embeddedBean); + } + + @Override + public Object origValue(int propertyIndex) { + return base.origValue(propertyIndex + offset); + } + + @Override + public int findProperty(String propertyName) { + return base.findProperty(propertyName); + } + + @Override + public String property(int propertyIndex) { + return base.property(propertyIndex + offset); + } + + @Override + public int propertyLength() { + return base.propertyLength(); + } + + @Override + public void setPropertyLoaded(String propertyName, boolean loaded) { + base.setPropertyLoaded(propertyName, loaded); + } + + @Override + public void setPropertyUnloaded(int propertyIndex) { + base.setPropertyUnloaded(propertyIndex + offset); + } + + @Override + public void setLoadedProperty(int propertyIndex) { + base.setLoadedProperty(propertyIndex + offset); + } + + @Override + public void setLoadedPropertyAll() { + base.setLoadedPropertyAll(); + } + + @Override + public boolean isLoadedProperty(int propertyIndex) { + return base.isLoadedProperty(propertyIndex + offset); + } + + @Override + public boolean isChangedProperty(int propertyIndex) { + return base.isChangedProperty(propertyIndex + offset); + } + + @Override + public boolean isDirtyProperty(int propertyIndex) { + return base.isDirtyProperty(propertyIndex + offset); + } + + @Override + public void markPropertyAsChanged(int propertyIndex) { + base.markPropertyAsChanged(propertyIndex + offset); + } + + @Override + public void setChangedProperty(int propertyIndex) { + base.setChangedProperty(propertyIndex + offset); + } + + @Override + public void setChangeLoaded(int propertyIndex) { + base.setChangeLoaded(propertyIndex + offset); + } + + @Override + public void setEmbeddedPropertyDirty(int propertyIndex) { + base.setEmbeddedPropertyDirty(propertyIndex + offset); + } + + @Override + public void setOriginalValue(int propertyIndex, Object value) { + base.setOriginalValue(propertyIndex + offset, value); + } + + @Override + public void setOriginalValueForce(int propertyIndex, Object value) { + base.setOriginalValueForce(propertyIndex + offset, value); + } + + @Override + public void setNewBeanForUpdate() { + base.setNewBeanForUpdate(); + } + + @Override + public Set loadedPropertyNames() { + return base.loadedPropertyNames(); + } + + @Override + public boolean[] dirtyProperties() { + return base.dirtyProperties(); + } + + @Override + public Set dirtyPropertyNames() { + return base.dirtyPropertyNames(); + } + + @Override + public void addDirtyPropertyNames(Set props, String prefix) { + base.addDirtyPropertyNames(props, prefix); + } + + @Override + public boolean hasDirtyProperty(Set propertyNames) { + return base.hasDirtyProperty(propertyNames); + } + + @Override + public Map dirtyValues() { + return base.dirtyValues(); + } + + @Override + public void addDirtyPropertyValues(Map dirtyValues, String prefix) { + base.addDirtyPropertyValues(dirtyValues, prefix); + } + + @Override + public void addDirtyPropertyValues(BeanDiffVisitor visitor) { + base.addDirtyPropertyValues(visitor); + } + + @Override + public StringBuilder dirtyPropertyKey() { + return base.dirtyPropertyKey(); + } + + @Override + public void addDirtyPropertyKey(StringBuilder sb) { + base.addDirtyPropertyKey(sb); + } + + @Override + public StringBuilder loadedPropertyKey() { + return base.loadedPropertyKey(); + } + + @Override + public boolean[] loaded() { + return base.loaded(); + } + + @Override + public int lazyLoadPropertyIndex() { + return base.lazyLoadPropertyIndex() - offset; + } + + @Override + public String lazyLoadProperty() { + return base.lazyLoadProperty(); + } + + @Override + public void loadBean(int loadProperty) { + base.loadBean(loadProperty); + } + + @Override + public void loadBeanInternal(int loadProperty, BeanLoader loader) { + base.loadBeanInternal(loadProperty + offset, loader); + } + + @Override + public void initialisedMany(int propertyIndex) { + base.initialisedMany(propertyIndex + offset); + } + + @Override + public void preGetterCallback(int propertyIndex) { + base.preGetterCallback(propertyIndex + offset); + } + + @Override + public void preGetId() { + base.preGetId(); + } + + @Override + public void preGetter(int propertyIndex) { + base.preGetter(propertyIndex + offset); + } + + @Override + public void preSetterMany(boolean interceptField, int propertyIndex, Object oldValue, Object newValue) { + base.preSetterMany(interceptField, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Object origValue) { + base.setChangedPropertyValue(propertyIndex + offset, setDirtyState, origValue); + } + + @Override + public void setDirtyStatus() { + base.setDirtyStatus(); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, Object oldValue, Object newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, boolean oldValue, boolean newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, int oldValue, int newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, long oldValue, long newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, double oldValue, double newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, float oldValue, float newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, short oldValue, short newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, char oldValue, char newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, byte oldValue, byte newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, char[] oldValue, char[] newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, byte[] oldValue, byte[] newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void setOldValue(int propertyIndex, Object oldValue) { + base.setOldValue(propertyIndex + offset, oldValue); + } + + @Override + public int sortOrder() { + return base.sortOrder(); + } + + @Override + public void setSortOrder(int sortOrder) { + base.setSortOrder(sortOrder); + } + + @Override + public void setDeletedFromCollection(boolean deletedFromCollection) { + base.setDeletedFromCollection(deletedFromCollection); + } + + @Override + public boolean isOrphanDelete() { + return base.isOrphanDelete(); + } + + @Override + public void setLoadError(int propertyIndex, Exception t) { + base.setLoadError(propertyIndex + offset, t); + } + + @Override + public Map loadErrors() { + return base.loadErrors(); + } + + @Override + public boolean isChangedProp(int propertyIndex) { + return base.isChangedProp(propertyIndex + offset); + } + + @Override + public MutableValueInfo mutableInfo(int propertyIndex) { + return base.mutableInfo(propertyIndex + offset); + } + + @Override + public void mutableInfo(int propertyIndex, MutableValueInfo info) { + base.mutableInfo(propertyIndex + offset, info); + } + + @Override + public void mutableNext(int propertyIndex, MutableValueNext next) { + base.mutableNext(propertyIndex + offset, next); + } + + @Override + public String mutableNext(int propertyIndex) { + return base.mutableNext(propertyIndex + offset); + } + + @Override + public Object value(int propertyIndex) { + return base.value(propertyIndex + offset); + } + + @Override + public Object valueIntercept(int propertyIndex) { + return base.valueIntercept(propertyIndex + offset); + } + + @Override + public void setValue(int propertyIndex, Object value) { + base.setValue(propertyIndex + offset, value); + } + + @Override + public void setValueIntercept(int propertyIndex, Object value) { + base.setValueIntercept(propertyIndex + offset, value); + } +} diff --git a/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java new file mode 100644 index 0000000000..cfea5db354 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java @@ -0,0 +1,34 @@ +package io.ebean.bean; + +import io.ebean.bean.extend.ExtendableBean; + +/** + * Provides access to the EntityExtensions. Each ExtendableBean may have multiple Extension-Accessors stored in the static + * {@link ExtensionAccessors} per class level. + *

+ * This interface is internally used by the enhancer. + * + * @author Roland Praml, FOCONIS AG + */ +public interface ExtensionAccessor { + + /* + * Returns the extension for a given bean. + */ + EntityBean getExtension(ExtendableBean bean); + + /** + * Returns the index of this extension. + */ + int getIndex(); + + /** + * Return the type of this extension. + */ + Class getType(); + + /** + * Returns the additional properties of this extension. + */ + String[] getProperties(); +} diff --git a/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java new file mode 100644 index 0000000000..2a693bc97e --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java @@ -0,0 +1,265 @@ +package io.ebean.bean; + +import io.ebean.bean.extend.EntityExtension; +import io.ebean.bean.extend.ExtendableBean; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Each ExtendableBean has one static member defined as + *

+ * private static ExtensionAccessors _ebean_extension_accessors =
+ *   new ExtensionAccessors(thisClass._ebeanProps, superClass._ebean_extension_accessors | null)
+ * 
+ * The ExtensionAccessors class is used to compute the additional space, that has to be reserved + * in the descriptor and the virtual properties, that will be added to the bean descriptor. + * The following class structure: + *
+ *   @Entity
+ *   class Base extends ExtendableBean {
+ *     String prop0;
+ *     String prop1;
+ *     String prop2;
+ *   }
+ *   @EntityExtends(Base.class)
+ *   class Ext1 {
+ *     String prop3;
+ *     String prop4;
+ *   }
+ *   @EntityExtends(Base.class)
+ *   class Ext2 {
+ *     String prop5;
+ *     String prop6;
+ *   }
+ * 
+ * will create an EntityBeanIntercept for "Base" holding up to 7 fields. Writing to fields 0..2 with ebi.setValue will modify + * the fields in Base, the r/w accesses to fields 3..4 are routed to Ext1 and 5..6 to Ext2. + *

+ * Note about offset and index: + *

+ *

+ * When you have subclasses (class Child extends Base) the extensions have all the same index in the parent and in + * the subclass, but may have different offsets, as the Child-class will provide additional fields. + *

+ * + * @author Roland Praml, FOCONIS AG + */ +public class ExtensionAccessors implements Iterable { + + /** + * Default extension info for beans, that have no extension. + */ + public static ExtensionAccessors NONE = new ExtensionAccessors(); + + /** + * The start offset specifies the offset where the first extension will start + */ + private final int startOffset; + + /** + * The entries. + */ + private List accessors = new ArrayList<>(); + + /** + * If we inherit from a class that has extensions, we have to inherit also all extensions from here + */ + private final ExtensionAccessors parent; + + /** + * The total property length of all extensions. This will be initialized once and cannot be changed any more + */ + private volatile int propertyLength = -1; + + /** + * The offsets where the extensions will start for effective binary search. + */ + private int[] offsets; + + /** + * Lock for synchronizing the initialization. + */ + private static final Lock lock = new ReentrantLock(); + + /** + * Constructor for ExtensionInfo.NONE. + */ + private ExtensionAccessors() { + this.startOffset = Integer.MAX_VALUE; + this.propertyLength = 0; + this.parent = null; + } + + /** + * Called from enhancer. Each entity has a static field initialized with + * _ebean_extensions = new ExtensonInfo(thisClass._ebeanProps, superClass._ebean_extensions | null) + */ + public ExtensionAccessors(String[] props, ExtensionAccessors parent) { + this.startOffset = props.length; + this.parent = parent; + } + + /** + * Called from enhancer. Each class annotated with {@link EntityExtension} will show up here. + * + * @param prototype instance of the class that is annotated with {@link EntityExtension} + */ + public ExtensionAccessor add(EntityBean prototype) { + if (propertyLength != -1) { + throw new UnsupportedOperationException("The extension is already in use and cannot be extended anymore"); + } + Entry entry = new Entry(prototype); + lock.lock(); + try { + accessors.add(entry); + } finally { + lock.unlock(); + } + return entry; + } + + /** + * returns how many extensions are registered. + */ + public int size() { + init(); + return accessors.size(); + } + + /** + * Returns the additional properties, that have been added by extensions. + */ + public int getPropertyLength() { + init(); + return propertyLength; + } + + /** + * Copies parent extensions and initializes the offsets. This will be done once only. + */ + private void init() { + if (propertyLength != -1) { + return; + } + lock.lock(); + try { + if (propertyLength != -1) { + return; + } + if (parent != null) { + parent.init(); + if (accessors.isEmpty()) { + accessors = parent.accessors; + } else { + accessors.addAll(0, parent.accessors); + } + } + int length = 0; + offsets = new int[accessors.size()]; + for (int i = 0; i < accessors.size(); i++) { + Entry entry = (Entry) accessors.get(i); + entry.index = i; + offsets[i] = startOffset + length; + length += entry.getProperties().length; + } + propertyLength = length; + } finally { + lock.unlock(); + } + } + + /** + * Returns the offset of this extension accessor. + * Note: The offset may vary on subclasses + */ + int getOffset(ExtensionAccessor accessor) { + return offsets[accessor.getIndex()]; + } + + /** + * Finds the accessor for a given property. If the propertyIndex is lower than startOffset, no accessor will be returned, + * as this means that we try to access a property in the base-entity. + */ + ExtensionAccessor findAccessor(int propertyIndex) { + init(); + if (propertyIndex < startOffset) { + return null; + } + int pos = Arrays.binarySearch(offsets, propertyIndex); + if (pos == -1) { + return null; + } + if (pos < 0) { + pos = -2 - pos; + } + return accessors.get(pos); + } + + @Override + public Iterator iterator() { + init(); + return accessors.iterator(); + } + + /** + * Invoked by enhancer. + */ + public EntityBean createInstance(ExtensionAccessor accessor, EntityBean base) { + int offset = getOffset(accessor); + return ((Entry) accessor).createInstance(offset, base); + } + + static class Entry implements ExtensionAccessor { + private int index; + private final EntityBean prototype; + + private Entry(EntityBean prototype) { + this.prototype = prototype; + } + + @Override + public String[] getProperties() { + return prototype._ebean_getPropertyNames(); + } + + @Override + public int getIndex() { + return index; + } + + @Override + public Class getType() { + return prototype.getClass(); + } + + EntityBean createInstance(int offset, EntityBean base) { + return (EntityBean) prototype._ebean_newExtendedInstance(offset, base); + } + + @Override + public EntityBean getExtension(ExtendableBean bean) { + EntityBean eb = (EntityBean) bean; + return eb._ebean_getExtension(Entry.this); + } + } + + /** + * Reads the extension accessors for a given class. If the provided type is not an ExtenadableBean, the + * ExtensionAccessors.NONE is returned. + */ + public static ExtensionAccessors read(Class type) { + if (ExtendableBean.class.isAssignableFrom(type)) { + try { + return (ExtensionAccessors) type.getField("_ebean_extension_accessors").get(null); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Could not read extension info from " + type, e); + } + } + return ExtensionAccessors.NONE; + } +} diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java new file mode 100644 index 0000000000..a38655100b --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java @@ -0,0 +1,116 @@ +package io.ebean.bean; + +/** + * Base class for InterceptReadOnly / InterceptReadWrite. This class should contain only the essential member variables to keep + * the memory footprint low. + * + * @author Roland Praml, FOCONIS AG + */ +public abstract class InterceptBase implements EntityBeanIntercept { + + /** + * The actual entity bean that 'owns' this intercept. + */ + protected final EntityBean owner; + + protected InterceptBase(EntityBean owner) { + this.owner = owner; + } + + protected ExtensionAccessor findAccessor(int index) { + return owner._ebean_getExtensionAccessors().findAccessor(index); + } + + private int getOffset(ExtensionAccessor accessor) { + return owner._ebean_getExtensionAccessors().getOffset(accessor); + } + + protected EntityBean getExtensionBean(ExtensionAccessor accessor) { + return owner._ebean_getExtension(accessor); + } + + @Override + public int findProperty(String propertyName) { + String[] names = owner._ebean_getPropertyNames(); + int i; + for (i = 0; i < names.length; i++) { + if (names[i].equals(propertyName)) { + return i; + } + } + for (ExtensionAccessor acc : owner._ebean_getExtensionAccessors()) { + names = acc.getProperties(); + for (int j = 0; j < names.length; j++) { + if (names[j].equals(propertyName)) { + return i; + } + i++; + } + } + return -1; + } + + @Override + public String property(int propertyIndex) { + if (propertyIndex == -1) { + return null; + } + ExtensionAccessor accessor = findAccessor(propertyIndex); + if (accessor == null) { + return owner._ebean_getPropertyName(propertyIndex); + } else { + int offset = getOffset(accessor); + return getExtensionBean(accessor)._ebean_getPropertyName(propertyIndex - offset); + } + } + + @Override + public int propertyLength() { + return owner._ebean_getPropertyNames().length + + owner._ebean_getExtensionAccessors().getPropertyLength(); + } + + @Override + public Object value(int index) { + ExtensionAccessor accessor = findAccessor(index); + if (accessor == null) { + return owner._ebean_getField(index); + } else { + int offset = getOffset(accessor); + return getExtensionBean(accessor)._ebean_getField(index - offset); + } + } + + @Override + public Object valueIntercept(int index) { + ExtensionAccessor accessor = findAccessor(index); + if (accessor == null) { + return owner._ebean_getFieldIntercept(index); + } else { + int offset = getOffset(accessor); + return getExtensionBean(accessor)._ebean_getFieldIntercept(index - offset); + } + } + + @Override + public void setValue(int index, Object value) { + ExtensionAccessor accessor = findAccessor(index); + if (accessor == null) { + owner._ebean_setField(index, value); + } else { + int offset = getOffset(accessor); + getExtensionBean(accessor)._ebean_setField(index - offset, value); + } + } + + @Override + public void setValueIntercept(int index, Object value) { + ExtensionAccessor accessor = findAccessor(index); + if (accessor == null) { + owner._ebean_setFieldIntercept(index, value); + } else { + int offset = getOffset(accessor); + getExtensionBean(accessor)._ebean_setFieldIntercept(index - offset, value); + } + } +} diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java index 26f5faf8d5..ee0809515f 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java @@ -13,15 +13,14 @@ * required for updates such as per property changed, loaded, dirty state, original values * bean state etc. */ -public class InterceptReadOnly implements EntityBeanIntercept { +public class InterceptReadOnly extends InterceptBase implements EntityBeanIntercept { - private final EntityBean owner; /** * Create with a given entity. */ public InterceptReadOnly(Object ownerBean) { - this.owner = (EntityBean) ownerBean; + super((EntityBean) ownerBean); } @Override @@ -234,22 +233,7 @@ public Object origValue(int propertyIndex) { return null; } - @Override - public int findProperty(String propertyName) { - return 0; - } - - @Override - public String property(int propertyIndex) { - return null; - } - - @Override - public int propertyLength() { - return 0; - } - - @Override + @Override public void setPropertyLoaded(String propertyName, boolean loaded) { } diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java index c760ca10b4..b08d310683 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java @@ -12,7 +12,12 @@ import java.io.InputStream; import java.math.BigDecimal; import java.net.URL; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -22,9 +27,8 @@ * This provides the mechanisms to support deferred fetching of reference beans * and oldValues generation for concurrency checking. */ -public final class InterceptReadWrite implements EntityBeanIntercept { - - private static final long serialVersionUID = -3664031775464862649L; +public final class InterceptReadWrite extends InterceptBase implements EntityBeanIntercept { + private static final long serialVersionUID = 1834735632647183821L; private static final int STATE_NEW = 0; private static final int STATE_REFERENCE = 1; @@ -55,11 +59,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept { private String ebeanServerName; private boolean deletedFromCollection; - - /** - * The actual entity bean that 'owns' this intercept. - */ - private final EntityBean owner; private EntityBean embeddedOwner; private int embeddedOwnerIndex; /** @@ -101,15 +100,15 @@ public final class InterceptReadWrite implements EntityBeanIntercept { * Create with a given entity. */ public InterceptReadWrite(Object ownerBean) { - this.owner = (EntityBean) ownerBean; - this.flags = new byte[owner._ebean_getPropertyNames().length]; + super((EntityBean) ownerBean); + this.flags = new byte[super.propertyLength()]; } /** * EXPERIMENTAL - Constructor only for use by serialization frameworks. */ public InterceptReadWrite() { - this.owner = null; + super(null); this.flags = null; } @@ -227,7 +226,7 @@ public boolean isDirty() { } if (mutableInfo != null) { for (int i = 0; i < mutableInfo.length; i++) { - if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) { + if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(value(i))) { dirty = true; break; } @@ -411,25 +410,6 @@ public Object origValue(int propertyIndex) { return origValues[propertyIndex]; } - @Override - public int findProperty(String propertyName) { - final String[] names = owner._ebean_getPropertyNames(); - for (int i = 0; i < names.length; i++) { - if (names[i].equals(propertyName)) { - return i; - } - } - return -1; - } - - @Override - public String property(int propertyIndex) { - if (propertyIndex == -1) { - return null; - } - return owner._ebean_getPropertyName(propertyIndex); - } - @Override public int propertyLength() { return flags.length; @@ -571,7 +551,7 @@ public void addDirtyPropertyNames(Set props, String prefix) { props.add((prefix == null ? property(i) : prefix + property(i))); } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { // an embedded property has been changed - recurse - final EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i); + final EntityBean embeddedBean = (EntityBean) value(i); embeddedBean._ebean_getIntercept().addDirtyPropertyNames(props, property(i) + "."); } } @@ -579,9 +559,10 @@ public void addDirtyPropertyNames(Set props, String prefix) { @Override public boolean hasDirtyProperty(Set propertyNames) { - final String[] names = owner._ebean_getPropertyNames(); + String[] names = owner._ebean_getPropertyNames(); final int len = propertyLength(); - for (int i = 0; i < len; i++) { + int i; + for (i = 0; i < len; i++) { if (isChangedProp(i)) { if (propertyNames.contains(names[i])) { return true; @@ -592,6 +573,22 @@ public boolean hasDirtyProperty(Set propertyNames) { } } } + for (ExtensionAccessor acc : owner._ebean_getExtensionAccessors()) { + names = acc.getProperties(); + for (int j = 0; j < names.length; j++) { + if (isChangedProp(i)) { + if (propertyNames.contains(names[j])) { + return true; + } + } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { + if (propertyNames.contains(names[j])) { + return true; + } + } + i++; + } + } + return false; } @@ -609,7 +606,7 @@ public void addDirtyPropertyValues(Map dirtyValues, String pr if (isChangedProp(i)) { // the property has been changed on this bean final String propName = (prefix == null ? property(i) : prefix + property(i)); - final Object newVal = owner._ebean_getField(i); + final Object newVal = value(i); final Object oldVal = origValue(i); if (notEqual(oldVal, newVal)) { dirtyValues.put(propName, new ValuePair(newVal, oldVal)); @@ -628,7 +625,7 @@ public void addDirtyPropertyValues(BeanDiffVisitor visitor) { for (int i = 0; i < len; i++) { if (isChangedProp(i)) { // the property has been changed on this bean - final Object newVal = owner._ebean_getField(i); + final Object newVal = value(i); final Object oldVal = origValue(i); if (notEqual(oldVal, newVal)) { visitor.visit(i, newVal, oldVal); @@ -1036,7 +1033,7 @@ public Map loadErrors() { public boolean isChangedProp(int i) { if ((flags[i] & FLAG_CHANGED_PROP) != 0) { return true; - } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) { + } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(value(i))) { return false; } else { // mark for change diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java new file mode 100644 index 0000000000..c62d099a4e --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java @@ -0,0 +1,70 @@ +package io.ebean.bean.extend; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation for a class to extend an existing class. + *

+ * Normally, you would have annotated like the following example + * + *

+ *   package basePkg;
+ *   import extPkg.MyExtEntity;
+ *   class MyBaseEntity {
+ *     // this line is mandatory, to allow deletion of MyBaseEntity
+ *     @OneToOne(optional = true, cascade = Cascade.ALL)
+ *     private MyExtEntity
+ *   }
+ *
+ *   package extPkg;
+ *   import basePkg.myBaseEntity;
+ *   class MyExtEntity {
+ *     @OneToOne(optional = false)
+ *     private MyBaseEntity
+ *   }
+ * 
+ *

+ * If you spread your code over different packages or (especially in different maven modules), you'll get problems, because you'll get cyclic depencencies. + *

+ * To break up these dependencies, you can annotate 'MyExtEntity' + * + *

+ *   package extPkg;
+ *   import basePkg.myBaseEntity;
+ *   @EntityExtension(MyBaseEntity.class)
+ *   class MyExtEntity {
+ *     @OneToOne(optional = false)
+ *     private MyBaseEntity
+ *
+ *     private String someField;
+ *   }
+ * 
+ * This will create a virtual property in the MyBaseEntity without adding a dependency to MyExtEntity. + *

+ * You may add a + *

+ *   public static MyExtEntity get(MyBaseEntity base) {
+ *     throw new NotEnhancedException();
+ *   }
+ * 
+ * This getter will be replaced by the enhancer, so that you can easily get it with + * MyExtEntiy.get(base).getSomeField(). + *
+ * Technically, the instance of MyExtEntiy is stored in the _ebean_extension_storage array of + * MyBaseEntity. + *

+ * If you save the MyBaseEntity, it will also save the data stored in MyExtEntity. + * + * @author Alexander Wagner, FOCONIS AG + */ +@Documented +@Target(TYPE) +@Retention(RUNTIME) +public @interface EntityExtension { + Class[] value(); +} diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java new file mode 100644 index 0000000000..cb491c3508 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java @@ -0,0 +1,10 @@ +package io.ebean.bean.extend; + +/** + * Marker for EntityExtension superclass. + * + * @author Noemi Praml, FOCONIS AG + */ +public @interface EntityExtensionSuperclass { + +} diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java b/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java new file mode 100644 index 0000000000..1f4f4b7de1 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java @@ -0,0 +1,20 @@ +package io.ebean.bean.extend; + +import io.ebean.bean.EntityBean; +import io.ebean.bean.NotEnhancedException; + +/** + * Marker interface for beans that can be extended with @EntityExtension. + * + * @author Roland Praml, FOCONIS AG + */ +public interface ExtendableBean { + + /** + * Returns an array of registered extensions. This may be useful for bean validation. + * NOTE: The passed array should NOT be modified. + */ + default EntityBean[] _ebean_getExtensions() { + throw new NotEnhancedException(); + } +} diff --git a/ebean-api/src/main/java/io/ebean/common/BeanList.java b/ebean-api/src/main/java/io/ebean/common/BeanList.java index 12d0770d62..8fa03659fb 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanList.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanList.java @@ -8,14 +8,14 @@ /** * List capable of lazy loading and modification awareness. */ -public final class BeanList extends AbstractBeanCollection implements List, BeanCollectionAdd { +public class BeanList extends AbstractBeanCollection implements List, BeanCollectionAdd { private static final long serialVersionUID = 1L; /** * The underlying List implementation. */ - private List list; + List list; /** * Specify the underlying List implementation. @@ -111,30 +111,30 @@ public boolean checkEmptyLazyLoad() { } } + protected void initList(boolean skipLoad, boolean onlyIds) { + if (skipLoad) { + list = new ArrayList<>(); + } else { + lazyLoadCollection(onlyIds); + } + } + private void initClear() { lock.lock(); try { if (list == null) { - if (!disableLazyLoad && modifyListening) { - lazyLoadCollection(true); - } else { - list = new ArrayList<>(); - } + initList(disableLazyLoad || !modifyListening, true); } } finally { lock.unlock(); } } - private void init() { + void init() { lock.lock(); try { if (list == null) { - if (disableLazyLoad) { - list = new ArrayList<>(); - } else { - lazyLoadCollection(false); - } + initList(disableLazyLoad, false); } } finally { lock.unlock(); @@ -164,7 +164,10 @@ public Collection actualDetails() { } @Override - public Collection actualEntries() { + public Collection actualEntries(boolean load) { + if (load) { + init(); + } return list; } diff --git a/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java b/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java new file mode 100644 index 0000000000..8ffa4b47df --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java @@ -0,0 +1,133 @@ +package io.ebean.common; + +import io.ebean.bean.BeanCollection; +import io.ebean.bean.BeanCollectionLoader; +import io.ebean.bean.EntityBean; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * This bean list can perform additions without populating the list. + * This might be useful, if you just want to add entries to an existing collection. + * Works only for lists and only if there is no order column + */ +public class BeanListLazyAdd extends BeanList { + + public BeanListLazyAdd(BeanCollectionLoader loader, EntityBean ownerBean, String propertyName) { + super(loader, ownerBean, propertyName); + } + + private List lazyAddedEntries; + + @Override + public boolean add(E bean) { + checkReadOnly(); + + lock.lock(); + try { + if (list == null) { + // list is not yet initialized, so we may add elements to a spare list + if (lazyAddedEntries == null) { + lazyAddedEntries = new ArrayList<>(); + } + lazyAddedEntries.add(bean); + } else { + list.add(bean); + } + } finally { + lock.unlock(); + } + + if (modifyListening) { + modifyAddition(bean); + } + return true; + } + + @Override + public boolean addAll(Collection beans) { + checkReadOnly(); + + lock.lock(); + try { + if (list == null) { + // list is not yet initialized, so we may add elements to a spare list + if (lazyAddedEntries == null) { + lazyAddedEntries = new ArrayList<>(); + } + lazyAddedEntries.addAll(beans); + } else { + list.addAll(beans); + } + } finally { + lock.unlock(); + } + + if (modifyListening) { + getModifyHolder().modifyAdditionAll(beans); + } + + return true; + } + + + @Override + public void loadFrom(BeanCollection other) { + super.loadFrom(other); + if (lazyAddedEntries != null) { + list.addAll(lazyAddedEntries); + lazyAddedEntries = null; + } + } + + /** + * on init, this happens on all accessor methods except on 'add' and addAll, + * we add the lazy added entries at the end of the list + */ + @Override + protected void initList(boolean skipLoad, boolean onlyIds) { + if (skipLoad) { + if (lazyAddedEntries != null) { + list = lazyAddedEntries; + lazyAddedEntries = null; + } else { + list = new ArrayList<>(); + } + } else { + lazyLoadCollection(onlyIds); + if (lazyAddedEntries != null) { + list.addAll(lazyAddedEntries); + lazyAddedEntries = null; + } + } + } + + @Override + public List getLazyAddedEntries(boolean reset) { + List ret = lazyAddedEntries; + if (reset) { + lazyAddedEntries = null; + } + return ret; + } + + @Override + public boolean isSkipSave() { + return lazyAddedEntries == null && super.isSkipSave(); + } + + public boolean checkEmptyLazyLoad() { + if (list != null) { + return false; + } else if (lazyAddedEntries == null) { + list = new ArrayList<>(); + return true; + } else { + list = lazyAddedEntries; + lazyAddedEntries = null; + return false; + } + } +} diff --git a/ebean-api/src/main/java/io/ebean/common/BeanMap.java b/ebean-api/src/main/java/io/ebean/common/BeanMap.java index fe959c22c0..d1c5801e38 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanMap.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanMap.java @@ -187,7 +187,10 @@ public Collection actualDetails() { * Returns the map entrySet. */ @Override - public Collection actualEntries() { + public Collection actualEntries(boolean load) { + if (load) { + init(); + } return map.entrySet(); } diff --git a/ebean-api/src/main/java/io/ebean/common/BeanSet.java b/ebean-api/src/main/java/io/ebean/common/BeanSet.java index 81d72284b8..05451657f4 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanSet.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanSet.java @@ -165,7 +165,10 @@ public Collection actualDetails() { } @Override - public Collection actualEntries() { + public Collection actualEntries(boolean load) { + if (load) { + init(); + } return set; } diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java index 8c25f20143..be74062651 100644 --- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java @@ -17,6 +17,7 @@ import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; import io.ebean.meta.MetricNamingMatch; +import io.ebean.plugin.CustomDeployParser; import io.ebean.util.StringHelper; import javax.persistence.EnumType; @@ -397,6 +398,8 @@ public class DatabaseConfig { */ private Clock clock = Clock.systemUTC(); + private TempFileProvider tempFileProvider = new WeakRefTempFileProvider(); + private List idGenerators = new ArrayList<>(); private List findControllers = new ArrayList<>(); private List persistControllers = new ArrayList<>(); @@ -406,6 +409,7 @@ public class DatabaseConfig { private List queryAdapters = new ArrayList<>(); private final List bulkTableEventListeners = new ArrayList<>(); private final List configStartupListeners = new ArrayList<>(); + private final List customDeployParsers = new ArrayList<>(); /** * By default inserts are included in the change log. @@ -441,6 +445,8 @@ public class DatabaseConfig { private int backgroundExecutorShutdownSecs = 30; private BackgroundExecutorWrapper backgroundExecutorWrapper = new MdcBackgroundExecutorWrapper(); + private boolean tenantPartitionedCache; + // defaults for the L2 bean caching private int cacheMaxSize = 10000; @@ -500,6 +506,11 @@ public class DatabaseConfig { */ private boolean queryPlanEnable; + /** + * Additional platform specific options for query-plan generation. + */ + private String queryPlanOptions; + /** * The default threshold in micros for collecting query plans. */ @@ -536,6 +547,10 @@ public class DatabaseConfig { */ private List mappingLocations = new ArrayList<>(); + /** + * The maximum string size in clob fields. + */ + private int maxStringSize = 0; /** * When true we do not need explicit GeneratedValue mapping. */ @@ -567,6 +582,14 @@ public void setClock(final Clock clock) { this.clock = clock; } + public TempFileProvider getTempFileProvider() { + return tempFileProvider; + } + + public void setTempFileProvider(final TempFileProvider tempFileProvider) { + this.tempFileProvider = tempFileProvider; + } + /** * Return the slow query time in millis. */ @@ -1491,6 +1514,21 @@ public void setBackgroundExecutorWrapper(BackgroundExecutorWrapper backgroundExe this.backgroundExecutorWrapper = backgroundExecutorWrapper; } + /** + * Returns, if the caches are partitioned by tenant. + */ + public boolean isTenantPartitionedCache() { + return tenantPartitionedCache; + } + + /** + * Sets the tenant partitioning mode for caches. This means, caches are created on demand, + * but they may not get invalidated across tenant boundaries * + */ + public void setTenantPartitionedCache(boolean tenantPartitionedCache) { + this.tenantPartitionedCache = tenantPartitionedCache; + } + /** * Return the L2 cache default max size. */ @@ -2714,6 +2752,17 @@ public List getServerConfigStartupListeners() { return configStartupListeners; } + /** + * Add a CustomDeployParser. + */ + public void addCustomDeployParser(CustomDeployParser customDeployParser) { + customDeployParsers.add(customDeployParser); + } + + public List getCustomDeployParsers() { + return customDeployParsers; + } + /** * Register all the BeanPersistListener instances. *

@@ -2884,6 +2933,7 @@ protected void loadSettings(PropertiesWrapper p) { queryPlanTTLSeconds = p.getInt("queryPlanTTLSeconds", queryPlanTTLSeconds); slowQueryMillis = p.getLong("slowQueryMillis", slowQueryMillis); queryPlanEnable = p.getBoolean("queryPlan.enable", queryPlanEnable); + queryPlanOptions = p.get("queryPlan.options", queryPlanOptions); queryPlanThresholdMicros = p.getLong("queryPlan.thresholdMicros", queryPlanThresholdMicros); queryPlanCapture = p.getBoolean("queryPlan.capture", queryPlanCapture); queryPlanCapturePeriodSecs = p.getLong("queryPlan.capturePeriodSecs", queryPlanCapturePeriodSecs); @@ -2898,6 +2948,7 @@ protected void loadSettings(PropertiesWrapper p) { useValidationNotNull = p.getBoolean("useValidationNotNull", useValidationNotNull); autoReadOnlyDataSource = p.getBoolean("autoReadOnlyDataSource", autoReadOnlyDataSource); idGeneratorAutomatic = p.getBoolean("idGeneratorAutomatic", idGeneratorAutomatic); + maxStringSize = p.getInt("maxStringSize", maxStringSize); backgroundExecutorSchedulePoolSize = p.getInt("backgroundExecutorSchedulePoolSize", backgroundExecutorSchedulePoolSize); backgroundExecutorShutdownSecs = p.getInt("backgroundExecutorShutdownSecs", backgroundExecutorShutdownSecs); @@ -2973,6 +3024,15 @@ protected void loadSettings(PropertiesWrapper p) { ddlPlaceholders = p.get("ddl.placeholders", ddlPlaceholders); ddlHeader = p.get("ddl.header", ddlHeader); + tenantPartitionedCache = p.getBoolean("tenantPartitionedCache", tenantPartitionedCache); + + cacheMaxIdleTime = p.getInt("cacheMaxIdleTime", cacheMaxIdleTime); + cacheMaxSize = p.getInt("cacheMaxSize", cacheMaxSize); + cacheMaxTimeToLive = p.getInt("cacheMaxTimeToLive", cacheMaxTimeToLive); + queryCacheMaxIdleTime = p.getInt("queryCacheMaxIdleTime", queryCacheMaxIdleTime); + queryCacheMaxSize = p.getInt("queryCacheMaxSize", queryCacheMaxSize); + queryCacheMaxTimeToLive = p.getInt("queryCacheMaxTimeToLive", queryCacheMaxTimeToLive); + // read tenant-configuration from config: // tenant.mode = NONE | DB | SCHEMA | CATALOG | PARTITION String mode = p.get("tenant.mode"); @@ -3261,6 +3321,21 @@ public void setIdGeneratorAutomatic(boolean idGeneratorAutomatic) { this.idGeneratorAutomatic = idGeneratorAutomatic; } + /** + * When maxStringSize is set, the string length in binds is limited to this size. If the string exceeds the given size, + * a Persistence exception is thrown. This is to handle DoS attacks or discover programming errors. + */ + public int getMaxStringSize() { + return maxStringSize; + } + + /** + * Setst the maximum size a string can have in bind arguments. See {@link #getMaxStringSize()} + */ + public void setMaxStringSize(int maxStringSize) { + this.maxStringSize = maxStringSize; + } + /** * Return true if query plan capture is enabled. */ @@ -3275,6 +3350,20 @@ public void setQueryPlanEnable(boolean queryPlanEnable) { this.queryPlanEnable = queryPlanEnable; } + /** + * Returns platform specific query plan options. + */ + public String getQueryPlanOptions() { + return queryPlanOptions; + } + + /** + * Set platform specific query plan options. + */ + public void setQueryPlanOptions(String queryPlanOptions) { + this.queryPlanOptions = queryPlanOptions; + } + /** * Return the query plan collection threshold in microseconds. */ diff --git a/ebean-api/src/main/java/io/ebean/config/DeleteOnShutdownTempFileProvider.java b/ebean-api/src/main/java/io/ebean/config/DeleteOnShutdownTempFileProvider.java new file mode 100644 index 0000000000..ad257c98ee --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/config/DeleteOnShutdownTempFileProvider.java @@ -0,0 +1,68 @@ +package io.ebean.config; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TempFileProvider implementation, which deletes all temp files on shutdown. + * + * @author Roland Praml, FOCONIS AG + * + */ +public class DeleteOnShutdownTempFileProvider implements TempFileProvider { + + private static final Logger logger = LoggerFactory.getLogger(DeleteOnShutdownTempFileProvider.class); + + List tempFiles = new ArrayList<>(); + private final String prefix; + private final String suffix; + private final File directory; + + /** + * Creates the TempFileProvider with default prefix "db-". + */ + public DeleteOnShutdownTempFileProvider() { + this("db-", null, null); + } + + /** + * Creates the TempFileProvider. + */ + public DeleteOnShutdownTempFileProvider(String prefix, String suffix, File directory) { + this.prefix = prefix; + this.suffix = suffix; + this.directory = directory; + } + + @Override + public File createTempFile() throws IOException { + File file = File.createTempFile(prefix, suffix, directory); + synchronized (tempFiles) { + tempFiles.add(file.getAbsolutePath()); + } + return file; + } + + /** + * Deletes all created files on shutdown. + */ + @Override + public void shutdown() { + synchronized (tempFiles) { + for (String path : tempFiles) { + if (new File(path).delete()) { + logger.trace("deleted {}", path); + } else { + logger.warn("could not delete {}", path); + } + } + tempFiles.clear(); + } + } + +} diff --git a/ebean-api/src/main/java/io/ebean/config/TempFileProvider.java b/ebean-api/src/main/java/io/ebean/config/TempFileProvider.java new file mode 100644 index 0000000000..4658b46c28 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/config/TempFileProvider.java @@ -0,0 +1,23 @@ +package io.ebean.config; + +import java.io.File; +import java.io.IOException; + +/** + * Creates a temp file for the ScalarTypeFile datatype. + * + * @author Roland Praml, FOCONIS AG + * + */ +public interface TempFileProvider { + + /** + * Creates a tempFile. + */ + File createTempFile() throws IOException; + + /** + * Shutdown the tempFileProvider. + */ + void shutdown(); +} diff --git a/ebean-api/src/main/java/io/ebean/config/WeakRefTempFileProvider.java b/ebean-api/src/main/java/io/ebean/config/WeakRefTempFileProvider.java new file mode 100644 index 0000000000..aa3ae82905 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/config/WeakRefTempFileProvider.java @@ -0,0 +1,145 @@ +package io.ebean.config; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * WeakRefTempFileProvider will delete the tempFile if all references to the returned File + * object are collected by the garbage collection. + * + * @author Roland Praml, FOCONIS AG + * + */ +public class WeakRefTempFileProvider implements TempFileProvider { + + private static final Logger logger = LoggerFactory.getLogger(WeakRefTempFileProvider.class); + + private final ReferenceQueue tempFiles = new ReferenceQueue<>(); + + private WeakFileReference root; + + private final String prefix; + private final String suffix; + private final File directory; + + /** + * We hold a linkedList of weak references. So we can remove stale files in O(1) + * + * @author Roland Praml, FOCONIS AG + */ + private static class WeakFileReference extends WeakReference { + + String path; + WeakFileReference prev; + WeakFileReference next; + + WeakFileReference(File referent, ReferenceQueue q) { + super(referent, q); + path = referent.getAbsolutePath(); + } + + boolean delete(boolean shutdown) { + File file = new File(path); + if (!file.exists()) { + logger.trace("already deleted {}", path); + return true; + } else if (file.delete()) { + logger.trace("deleted {}", path); + return true; + } else { + if (shutdown) { + logger.warn("could not delete {}", path); + } else { + logger.info("could not delete {} - will delete on shutdown", path); + } + return false; + } + } + } + + + /** + * Creates the TempFileProvider with default prefix "db-". + */ + public WeakRefTempFileProvider() { + this("db-", null, null); + } + + /** + * Creates the TempFileProvider. + */ + public WeakRefTempFileProvider(String prefix, String suffix, File directory) { + this.prefix = prefix; + this.suffix = suffix; + this.directory = directory; + } + + @Override + public File createTempFile() throws IOException { + File tempFile = File.createTempFile(prefix, suffix, directory); + logger.trace("createTempFile: {}", tempFile); + synchronized (this) { + add(new WeakFileReference(tempFile, tempFiles)); + } + return tempFile; + } + + /** + * Will delete stale files. + * This is public to use in tests. + */ + public void deleteStaleTempFiles() { + synchronized (this) { + deleteStaleTempFilesInternal(); + } + } + + private void deleteStaleTempFilesInternal() { + WeakFileReference ref; + while ((ref = (WeakFileReference) tempFiles.poll()) != null) { + if (ref.delete(false)) { + remove(ref); // remove from linkedList only, if delete was successful. + } + } + } + + private void add(WeakFileReference ref) { + deleteStaleTempFilesInternal(); + + if (root == null) { + root = ref; + } else { + ref.next = root; + root.prev = ref; + root = ref; + } + } + + private void remove(WeakFileReference ref) { + if (ref.next != null) { + ref.next.prev = ref.prev; + } + if (ref.prev != null) { + ref.prev.next = ref.next; + } else { + root = ref.next; + } + } + + /** + * Deletes all created files on shutdown. + */ + @Override + public void shutdown() { + while (root != null) { + root.delete(true); + root = root.next; + } + } + +} diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java index 7de8de1425..7408147018 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java @@ -673,7 +673,7 @@ public String fromForUpdate(Query.LockWait lockWait) { protected String withForUpdate(String sql, Query.LockWait lockWait, Query.LockType lockType) { // silently assume the database does not support the "for update" clause. - log.log(INFO, "it seems your database does not support the 'for update' clause"); + log.log(INFO, "it seems your database does not support the ''for update'' clause"); return sql; } diff --git a/ebean-api/src/main/java/io/ebean/meta/MetaInfoManager.java b/ebean-api/src/main/java/io/ebean/meta/MetaInfoManager.java index 81e7944f04..7f61640bff 100644 --- a/ebean-api/src/main/java/io/ebean/meta/MetaInfoManager.java +++ b/ebean-api/src/main/java/io/ebean/meta/MetaInfoManager.java @@ -64,5 +64,9 @@ default List collectMetricsAsData() { */ List queryPlanCollectNow(QueryPlanRequest request); + /** + * Creates a new MetricReportGenerator. This can be used to embed in your web-application + */ + MetricReportGenerator createReportGenerator(); } diff --git a/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java b/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java index c1ae44a823..4258b7c9ca 100644 --- a/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java +++ b/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java @@ -2,6 +2,8 @@ import io.ebean.ProfileLocation; +import java.time.Instant; + /** * Meta data for captured query plan. */ @@ -42,6 +44,11 @@ public interface MetaQueryPlan { */ String plan(); + /** + * The tenant ID of the plan. + */ + Object tenantId(); + /** * Return the query execution time associated with the bind values capture. */ @@ -51,4 +58,14 @@ public interface MetaQueryPlan { * Return the total count of times bind capture has occurred. */ long captureCount(); + + /** + * Return the time taken to capture this plan in microseconds. + */ + long captureMicros(); + + /** + * Return the instant when the bind values were captured. + */ + Instant whenCaptured(); } diff --git a/ebean-api/src/main/java/io/ebean/meta/MetricReportGenerator.java b/ebean-api/src/main/java/io/ebean/meta/MetricReportGenerator.java new file mode 100644 index 0000000000..054b77a56d --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/meta/MetricReportGenerator.java @@ -0,0 +1,22 @@ +package io.ebean.meta; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** + * The metric report. most likely a HTML report. + */ +public interface MetricReportGenerator { + + /** + * Writes the report to the outputStream. The stream will not be closed. + */ + void writeReport(OutputStream out) throws IOException; + + /** + * Used to configure the metric. This report dependent. + * Returns a string result, that should be sent back to the web application + */ + String configure(List values); +} diff --git a/ebean-api/src/main/java/io/ebean/meta/MetricReportValue.java b/ebean-api/src/main/java/io/ebean/meta/MetricReportValue.java new file mode 100644 index 0000000000..4ab5a2c9d0 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/meta/MetricReportValue.java @@ -0,0 +1,39 @@ +package io.ebean.meta; + +/** + * A Metric report value used to configure a metric report. + * This is most likely a REST call from the provided web-application + */ +public class MetricReportValue { + private String name; + private String value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int intValue() { + return Integer.valueOf(value); + } + + public MetricReportValue() { + + } + + public MetricReportValue(String name, Object value) { + this.name = name; + this.value = String.valueOf(value); + } +} diff --git a/ebean-api/src/main/java/io/ebean/metric/CountMetric.java b/ebean-api/src/main/java/io/ebean/metric/CountMetric.java index 3ce785c1ea..d125134552 100644 --- a/ebean-api/src/main/java/io/ebean/metric/CountMetric.java +++ b/ebean-api/src/main/java/io/ebean/metric/CountMetric.java @@ -5,7 +5,7 @@ /** * Metric for timed events like transaction execution times. */ -public interface CountMetric { +public interface CountMetric extends Metric { /** * Add to the counter. @@ -32,8 +32,4 @@ public interface CountMetric { */ void reset(); - /** - * Visit non empty metrics. - */ - void visit(MetricVisitor visitor); } diff --git a/ebean-api/src/main/java/io/ebean/metric/Metric.java b/ebean-api/src/main/java/io/ebean/metric/Metric.java new file mode 100644 index 0000000000..374b2134e2 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/metric/Metric.java @@ -0,0 +1,13 @@ +package io.ebean.metric; + +import io.ebean.meta.MetricVisitor; + +/** + * Base for all metrics. + */ +public interface Metric { + /** + * Visit the underlying metric. + */ + void visit(MetricVisitor visitor); +} diff --git a/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java b/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java index d5fb8bd282..478d0a9933 100644 --- a/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java +++ b/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java @@ -2,6 +2,9 @@ import io.ebean.ProfileLocation; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; + /** * Factory to create timed metric counters. */ @@ -29,6 +32,16 @@ static MetricFactory get() { */ CountMetric createCountMetric(String name); + /** + * Create a metric, that gets the value from a supplier. + */ + Metric createMetric(String name, LongSupplier supplier); + + /** + * Create a metric, that gets the value from a supplier. + */ + Metric createMetric(String name, IntSupplier supplier); + /** * Create a Timed metric. */ diff --git a/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java b/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java new file mode 100644 index 0000000000..d881320247 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java @@ -0,0 +1,31 @@ +package io.ebean.metric; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Global registry of custom metrics instances created. + */ +public final class MetricRegistry { + public interface RegistryEntry { + void remove(); + } + + private static final List list = Collections.synchronizedList(new ArrayList<>()); + + /** + * Register the metric instance. + */ + public static RegistryEntry register(Metric location) { + list.add(location); + return () -> list.remove(location); + } + + /** + * Return all the registered extra metrics. + */ + public static List registered() { + return list; + } +} diff --git a/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java b/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java index 24999ce0ed..c1f24eea45 100644 --- a/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java +++ b/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java @@ -5,15 +5,11 @@ /** * Internal Query plan metric holder. */ -public interface QueryPlanMetric { +public interface QueryPlanMetric extends Metric { /** * Return the underlying timed metric. */ TimedMetric metric(); - /** - * Visit the underlying metric. - */ - void visit(MetricVisitor visitor); } diff --git a/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java b/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java index 17c3defb95..4ec95581a4 100644 --- a/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java +++ b/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java @@ -5,7 +5,7 @@ /** * Metric for timed events like transaction execution times. */ -public interface TimedMetric { +public interface TimedMetric extends Metric { /** * Add a time event (usually in microseconds). @@ -37,8 +37,4 @@ public interface TimedMetric { */ TimedMetricStats collect(boolean reset); - /** - * Visit non empty metrics. - */ - void visit(MetricVisitor visitor); } diff --git a/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java b/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java index 5d10397ba2..aeff635e82 100644 --- a/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java +++ b/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java @@ -5,7 +5,7 @@ /** * A map of timed metrics keyed by a string. */ -public interface TimedMetricMap { +public interface TimedMetricMap extends Metric { /** * Add a time event given the start nanos. @@ -17,8 +17,4 @@ public interface TimedMetricMap { */ void add(String key, long exeMicros); - /** - * Visit the metric. - */ - void visit(MetricVisitor visitor); } diff --git a/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java new file mode 100644 index 0000000000..4c854f2f1e --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java @@ -0,0 +1,15 @@ +package io.ebean.plugin; + +import io.ebean.config.dbplatform.DatabasePlatform; + +/** + * Fired after all beans are parsed. You may implement own parsers to handle custom annotations. + * (See test case for example) + * + * @author Roland Praml, FOCONIS AG + */ +@FunctionalInterface +public interface CustomDeployParser { + + void parse(DeployBeanDescriptorMeta descriptor, DatabasePlatform databasePlatform); +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java new file mode 100644 index 0000000000..4347a5057c --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java @@ -0,0 +1,36 @@ +package io.ebean.plugin; + +import java.util.Collection; + +/** + * General deployment information. This is used in {@link CustomDeployParser}. + * + * @author Roland Praml, FOCONIS AG + */ +public interface DeployBeanDescriptorMeta { + + /** + * Return a collection of all BeanProperty deployment information. + */ + public Collection propertiesAll(); + + /** + * Get a BeanProperty by its name. + */ + public DeployBeanPropertyMeta getBeanProperty(String secondaryBeanName); + + /** + * Return the DeployBeanDescriptorMeta for the given bean class. + */ + public DeployBeanDescriptorMeta getDeployBeanDescriptorMeta(Class propertyType); + + /** + * Returns the discriminator column, if any. + * @return + */ + public String getDiscriminatorColumn(); + + public String getBaseTable(); + + DeployBeanPropertyMeta idProperty(); +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java new file mode 100644 index 0000000000..d7d1d637a3 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java @@ -0,0 +1,23 @@ +package io.ebean.plugin; + +public interface DeployBeanPropertyAssocMeta extends DeployBeanPropertyMeta { + + /** + * Return the mappedBy deployment attribute. + *

+ * This is the name of the property in the 'detail' bean that maps back to + * this 'master' bean. + *

+ */ + String getMappedBy(); + + /** + * Return the base table for this association. + *

+ * This has the table name which is used to determine the relationship for + * this association. + *

+ */ + String getBaseTable(); + +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java new file mode 100644 index 0000000000..4ba2d3e2a1 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java @@ -0,0 +1,37 @@ +package io.ebean.plugin; + +import java.lang.reflect.Field; + +public interface DeployBeanPropertyMeta { + + /** + * Return the name of the property. + */ + String getName(); + + /** + * The database column name this is mapped to. + */ + String getDbColumn(); + + /** + * Return the bean Field associated with this property. + */ + Field getField(); + + /** + * The property is based on a formula. + */ + void setSqlFormula(String sqlSelect, String sqlJoin); + + /** + * Return the bean type. + */ + Class getOwningType(); + + /** + * Return the property type. + */ + Class getPropertyType(); + +} diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java index 935b788600..7acdb976f1 100644 --- a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java +++ b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java @@ -12,18 +12,11 @@ */ public interface JsonBeanReader { - /** - * Read the JSON into given bean. Will update existing properties. - */ - T read(T target); - /** * Read the JSON returning a bean. */ - default T read() { - return read(null); - } - + T read(); + /** * Create a new reader taking the context from the existing one but using a new JsonParser. */ diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java index d2c0df822b..ad91620ccb 100644 --- a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java +++ b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import io.ebean.BeanMergeOptions; import io.ebean.FetchPath; import io.ebean.plugin.BeanType; +import io.ebean.plugin.Property; import java.io.IOException; import java.io.Reader; @@ -64,7 +66,9 @@ public interface JsonContext { * instances, so the object identity will not be preserved here. * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, JsonParser)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, JsonParser parser) throws JsonIOException; /** @@ -72,7 +76,9 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details modified. * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, JsonParser, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException; /** @@ -80,7 +86,9 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, Reader)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, Reader json) throws JsonIOException; /** @@ -88,7 +96,9 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details modified. * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, Reader, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, Reader json, JsonReadOptions options) throws JsonIOException; /** @@ -96,7 +106,9 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, String)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, String json) throws JsonIOException; /** @@ -104,9 +116,23 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, String, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, String json, JsonReadOptions options) throws JsonIOException; + /** + * Read json parser input and returns the property value. + * This can be used to read a single property (e.g. ID property) from a JSON stream + */ + T readProperty(Property property, JsonParser parser); + + /** + * Read json parser input and returns the property value.
+ * See {@link #readProperty(Property, JsonParser)} for details + */ + T readProperty(Property property, JsonParser parser, JsonReadOptions options); + /** * Create and return a new bean reading for the bean type given the JSON options and source. *

@@ -257,6 +283,16 @@ public interface JsonContext { */ String toJson(Object value, JsonWriteOptions options) throws JsonIOException; + /** + * Writes a single property of the current bean to generator. + */ + void writeProperty(Property property, Object bean, JsonGenerator generator) throws JsonIOException; + + /** + * Writes a single property of the current bean to generator. + */ + void writeProperty(Property property, Object bean, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException; + /** * Return true if the type is known as an Entity bean or a List Set or * Map of entity beans. diff --git a/ebean-api/src/main/java/module-info.java b/ebean-api/src/main/java/module-info.java index a8fb4ea387..cb7f9f7c7f 100644 --- a/ebean-api/src/main/java/module-info.java +++ b/ebean-api/src/main/java/module-info.java @@ -25,6 +25,7 @@ exports io.ebean; exports io.ebean.bean; + exports io.ebean.bean.extend; exports io.ebean.cache; exports io.ebean.meta; exports io.ebean.config; @@ -41,5 +42,5 @@ exports io.ebean.text; exports io.ebean.text.json; exports io.ebean.util; - + exports io.ebean.annotation.ext; } diff --git a/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java b/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java index 5eb17bbe87..5b78dbbe15 100644 --- a/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java +++ b/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java @@ -9,9 +9,9 @@ class EbeanVersionTest { @Test void checkMinAgentVersion_ok() { - assertFalse(EbeanVersion.checkMinAgentVersion("12.12.0")); - assertFalse(EbeanVersion.checkMinAgentVersion("12.12.99")); - assertFalse(EbeanVersion.checkMinAgentVersion("13.1.0")); + assertFalse(EbeanVersion.checkMinAgentVersion("13.10.0")); + assertFalse(EbeanVersion.checkMinAgentVersion("13.10.99")); + assertFalse(EbeanVersion.checkMinAgentVersion("14.1.0")); } @Test diff --git a/ebean-api/src/test/java/io/ebean/TestWeakRefTempFileProvider.java b/ebean-api/src/test/java/io/ebean/TestWeakRefTempFileProvider.java new file mode 100644 index 0000000000..dc69cc6876 --- /dev/null +++ b/ebean-api/src/test/java/io/ebean/TestWeakRefTempFileProvider.java @@ -0,0 +1,171 @@ +package io.ebean; + + +import io.ebean.config.WeakRefTempFileProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.channels.FileLock; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the WeakRefTempFileProvider. (Note: this test relies on an aggressive garbage collection. + * if GC implementation will change, the test may fail) + * + * @author Roland Praml, FOCONIS AG + */ +public class TestWeakRefTempFileProvider { + + WeakRefTempFileProvider prov = new WeakRefTempFileProvider(); + + @AfterEach + public void shutdown() { + prov.shutdown(); + } + + /** + * Run the garbage collection and delete stale files. + */ + private void gc() throws InterruptedException { + System.gc(); + Thread.sleep(100); + prov.deleteStaleTempFiles(); + } + + @Test + public void testStaleEntries() throws Exception { + File tempFile = prov.createTempFile(); + String fileName = tempFile.getAbsolutePath(); + + gc(); + + assertThat(new File(fileName)).exists(); + + tempFile = null; // give up reference + + gc(); + + assertThat(new File(fileName)).doesNotExist(); + + + } + + @Test + public void testLinkedListForward() throws Exception { + File tempFile1 = prov.createTempFile(); + String fileName1 = tempFile1.getAbsolutePath(); + File tempFile2 = prov.createTempFile(); + String fileName2 = tempFile2.getAbsolutePath(); + File tempFile3 = prov.createTempFile(); + String fileName3 = tempFile3.getAbsolutePath(); + + assertThat(new File(fileName1)).exists(); + assertThat(new File(fileName2)).exists(); + assertThat(new File(fileName3)).exists(); + + gc(); + + // give up first ref + tempFile1 = null; + + gc(); + + assertThat(new File(fileName1)).doesNotExist(); + assertThat(new File(fileName2)).exists(); + assertThat(new File(fileName3)).exists(); + + // give up second ref + tempFile2 = null; + + gc(); + + assertThat(new File(fileName1)).doesNotExist(); + assertThat(new File(fileName2)).doesNotExist(); + assertThat(new File(fileName3)).exists(); + + // give up third ref + tempFile3 = null; + + gc(); + + assertThat(new File(fileName1)).doesNotExist(); + assertThat(new File(fileName2)).doesNotExist(); + assertThat(new File(fileName3)).doesNotExist(); + + } + + + @Test + public void testLinkedListReverse() throws Exception { + File tempFile1 = prov.createTempFile(); + String fileName1 = tempFile1.getAbsolutePath(); + File tempFile2 = prov.createTempFile(); + String fileName2 = tempFile2.getAbsolutePath(); + File tempFile3 = prov.createTempFile(); + String fileName3 = tempFile3.getAbsolutePath(); + + assertThat(new File(fileName1)).exists(); + assertThat(new File(fileName2)).exists(); + assertThat(new File(fileName3)).exists(); + + gc(); + + // give up third ref + tempFile3 = null; + + gc(); + + assertThat(new File(fileName1)).exists(); + assertThat(new File(fileName2)).exists(); + assertThat(new File(fileName3)).doesNotExist(); + + // give up second ref + tempFile2 = null; + + gc(); + + assertThat(new File(fileName1)).exists(); + assertThat(new File(fileName2)).doesNotExist(); + assertThat(new File(fileName3)).doesNotExist(); + + // give up first ref + tempFile1 = null; + + gc(); + + assertThat(new File(fileName1)).doesNotExist(); + assertThat(new File(fileName2)).doesNotExist(); + assertThat(new File(fileName3)).doesNotExist(); + + } + + @Test + @Disabled("Runs on Windows only") + public void testFileLocked() throws Exception { + File tempFile = prov.createTempFile(); + String fileName = tempFile.getAbsolutePath(); + + try (FileOutputStream os = new FileOutputStream(fileName)) { + FileLock lock = os.getChannel().lock(); + try { + os.write(42); + + tempFile = null; + gc(); + } finally { + lock.release(); + } + + } + + assertThat(new File(fileName)).exists(); + + prov.shutdown(); + + assertThat(new File(fileName)).doesNotExist(); + } +} diff --git a/ebean-autotune/pom.xml b/ebean-autotune/pom.xml index d425dda80a..673ad765f2 100644 --- a/ebean-autotune/pom.xml +++ b/ebean-autotune/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -13,8 +13,8 @@ - scm:git:git@github.com:ebean-orm/ebean.git - HEAD + scm:git:git@github.com:FOCONIS/ebean.git + ebean-parent-13.6.4-FOC1 ebean autotune @@ -26,7 +26,7 @@ io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT provided @@ -62,7 +62,7 @@ io.ebean ebean-ddl-generator - 13.17.3 + 13.17.3-FOC12-SNAPSHOT test @@ -76,7 +76,7 @@ io.ebean ebean-platform-h2 - 13.17.3 + 13.17.3-FOC12-SNAPSHOT test diff --git a/ebean-bom/pom.xml b/ebean-bom/pom.xml index dbcf5300b7..16118d56ba 100644 --- a/ebean-bom/pom.xml +++ b/ebean-bom/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean bom @@ -89,112 +89,112 @@ io.ebean ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-core-type - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-joda-time - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-jackson-jsonnode - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-jackson-mapper - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-ddl-generator - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-externalmapping-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-externalmapping-xml - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-autotune - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-querybean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean querybean-generator - 13.17.3 + 13.17.3-FOC12-SNAPSHOT provided io.ebean kotlin-querybean-generator - 13.17.3 + 13.17.3-FOC12-SNAPSHOT provided io.ebean ebean-test - 13.17.3 + 13.17.3-FOC12-SNAPSHOT test io.ebean ebean-postgis - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-redis - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-spring-txn - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -202,67 +202,67 @@ io.ebean ebean-clickhouse - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-db2 - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-h2 - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-hana - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-mariadb - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-mysql - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-nuodb - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-oracle - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-postgres - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-sqlite - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-sqlserver - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml index 3dff6a7cfb..f4b89aa05f 100644 --- a/ebean-core-type/pom.xml +++ b/ebean-core-type/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-core-type @@ -16,7 +16,7 @@ io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml index c6fc054fb0..63d035fd4e 100644 --- a/ebean-core/pom.xml +++ b/ebean-core/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 13.17.3 + 13.17.3-FOC12-SNAPSHOT ebean-core @@ -14,15 +14,15 @@ https://ebean.io/ - scm:git:git@github.com:ebean-orm/ebean.git - HEAD + scm:git:git@github.com:FOCONIS/ebean.git + ebean-parent-13.6.4-FOC1 io.ebean ebean-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -46,13 +46,13 @@ io.ebean ebean-core-type - 13.17.3 + 13.17.3-FOC12-SNAPSHOT io.ebean ebean-externalmapping-api - 13.17.3 + 13.17.3-FOC12-SNAPSHOT @@ -143,21 +143,21 @@ io.ebean ebean-platform-h2 - 13.17.3 + 13.17.3-FOC12-SNAPSHOT test io.ebean ebean-platform-postgres - 13.17.3 + 13.17.3-FOC12-SNAPSHOT test io.ebean ebean-platform-sqlserver - 13.17.3 + 13.17.3-FOC12-SNAPSHOT test @@ -229,7 +229,6 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.1.1 Ebean 12 src/main/java/io/ebean/overview.html diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java index a207f3c573..9f77331016 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java @@ -7,13 +7,15 @@ public final class HashQuery { private final CQueryPlanKey planHash; private final BindValuesKey bindValuesKey; + private final Class dtoType; /** * Create the HashQuery. */ - public HashQuery(CQueryPlanKey planHash, BindValuesKey bindValuesKey) { + public HashQuery(CQueryPlanKey planHash, BindValuesKey bindValuesKey, Class dtoType) { this.planHash = planHash; this.bindValuesKey = bindValuesKey; + this.dtoType = dtoType; } @Override @@ -25,6 +27,7 @@ public String toString() { public int hashCode() { int hc = 92821 * planHash.hashCode(); hc = 92821 * hc + bindValuesKey.hashCode(); + hc = 92821 * hc + dtoType.hashCode(); return hc; } @@ -37,6 +40,6 @@ public boolean equals(Object obj) { return false; } HashQuery e = (HashQuery) obj; - return e.bindValuesKey.equals(bindValuesKey) && e.planHash.equals(planHash); + return e.bindValuesKey.equals(bindValuesKey) && e.planHash.equals(planHash) && e.dtoType.equals(dtoType); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java index 135976f540..c8a4210055 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java @@ -2,14 +2,16 @@ import io.ebean.meta.MetaQueryPlan; +import java.time.Instant; + /** * Internal database query plan being capture. */ public interface SpiDbQueryPlan extends MetaQueryPlan { /** - * Extend with queryTimeMicros and captureCount. + * Extend with queryTimeMicros, captureCount, captureMicros and when the bind values were captured. */ - SpiDbQueryPlan with(long queryTimeMicros, long captureCount); + SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured, Object tenantId); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java index b3c4858139..d821e75b84 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java @@ -12,4 +12,9 @@ public interface SpiDdlGenerator { */ void execute(boolean online); + /** + * Run DDL manually. This can be used to initialize multi tenant environments or if you plan not to run + * DDL on startup + */ + void runDdl(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java index bcef08e57e..344b428c9e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java @@ -1,5 +1,6 @@ package io.ebeaninternal.api; +import io.avaje.lang.Nullable; import io.ebean.*; import io.ebean.bean.BeanCollectionLoader; import io.ebean.bean.CallOrigin; @@ -14,7 +15,6 @@ import io.ebeaninternal.server.query.CQuery; import io.ebeaninternal.server.transaction.RemoteTransactionEvent; -import javax.annotation.Nullable; import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; @@ -197,6 +197,11 @@ public interface SpiEbeanServer extends SpiServer, ExtendedServer, BeanCollectio */ DataTimeZone dataTimeZone(); + /** + * Returns the maximum string size in bind values. + */ + int maxStringSize(); + /** * Check for slow query event. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java index e10b863152..f4e9d03fca 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -842,6 +842,11 @@ public static TemporalMode of(SpiQuery query) { */ void setManualId(); + /** + * Set the DTO type, that should be part of the queryHash. + */ + void setDtoType(Class dtoType); + /** * Set default select clauses where none have been explicitly defined. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java index a8acdb45a7..320c1d6faf 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java @@ -104,6 +104,11 @@ public interface SpiTransaction extends Transaction { */ Boolean isUpdateAllLoadedProperties(); + /** + * Returns true, if generated properties are overwritten (default) or are only set, if they are null. + */ + boolean isOverwriteGeneratedProperties(); + /** * Return the batchSize specifically set for this transaction or 0. *

diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java index 01311d1673..cc0ff69efa 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java @@ -60,6 +60,6 @@ public interface SpiTransactionManager { /** * Return a connection used for query plan collection. */ - Connection queryPlanConnection() throws SQLException; + Connection queryPlanConnection(Object tenantId) throws SQLException; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java index 87c2f606ab..88fc870aa8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java @@ -248,6 +248,16 @@ public void setUpdateAllLoadedProperties(boolean updateAllLoaded) { transaction.setUpdateAllLoadedProperties(updateAllLoaded); } + @Override + public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) { + transaction.setOverwriteGeneratedProperties(overwriteGeneratedProperties); + } + + @Override + public boolean isOverwriteGeneratedProperties() { + return transaction.isOverwriteGeneratedProperties(); + } + @Override public Boolean isUpdateAllLoadedProperties() { return transaction.isUpdateAllLoadedProperties(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java index 613b105cc6..4ed612defc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java @@ -34,5 +34,4 @@ public interface SpiJsonReader { Object readValueUsingObjectMapper(Class propertyType) throws IOException; - boolean update(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBind.java b/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBind.java index 3c4c1aedb8..89c5eebccb 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBind.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBind.java @@ -4,7 +4,11 @@ import io.ebeaninternal.api.CoreLog; import io.ebeaninternal.server.core.timezone.DataTimeZone; -import java.io.*; +import javax.persistence.PersistenceException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; import java.math.BigDecimal; import java.sql.*; import java.util.ArrayList; @@ -16,15 +20,19 @@ public class DataBind implements DataBinder { private final DataTimeZone dataTimeZone; + private final int maxStringSize; private final PreparedStatement pstmt; private final Connection connection; private final StringBuilder bindLog = new StringBuilder(); + private List inputStreams; protected int pos; private String json; - public DataBind(DataTimeZone dataTimeZone, PreparedStatement pstmt, Connection connection) { + + public DataBind(DataTimeZone dataTimeZone, int maxStringSize, PreparedStatement pstmt, Connection connection) { this.dataTimeZone = dataTimeZone; + this.maxStringSize = maxStringSize; this.pstmt = pstmt; this.connection = connection; } @@ -116,6 +124,10 @@ public final PreparedStatement getPstmt() { @Override public void setString(String value) throws SQLException { + if (maxStringSize > 0 && maxStringSize < value.length()) { + throw new PersistenceException("The value '" + value.substring(0, 50) + "...' (" + + value.length() + " chars) exceeds the max string size of " + maxStringSize + " chars)"); + } pstmt.setString(++pos, value); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBindCapture.java b/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBindCapture.java index 78924bafef..c2bbe0e21a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBindCapture.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBindCapture.java @@ -1,8 +1,8 @@ package io.ebeaninternal.server.bind; -import io.ebeaninternal.server.core.timezone.DataTimeZone; import io.ebeaninternal.server.bind.capture.BindCapture; import io.ebeaninternal.server.bind.capture.BindCaptureStatement; +import io.ebeaninternal.server.core.timezone.DataTimeZone; /** * Special DataBind used to capture bind values for obtaining explain plans. @@ -14,12 +14,12 @@ public final class DataBindCapture extends DataBind { /** * Create given the dataTimeZone in use. */ - public static DataBindCapture of(DataTimeZone dataTimeZone) { - return new DataBindCapture(dataTimeZone, new BindCaptureStatement()); + public static DataBindCapture of(DataTimeZone dataTimeZone, int maxStringSize) { + return new DataBindCapture(dataTimeZone, new BindCaptureStatement(), maxStringSize); } - private DataBindCapture(DataTimeZone dataTimeZone, BindCaptureStatement pstmt) { - super(dataTimeZone, pstmt, null); + private DataBindCapture(DataTimeZone dataTimeZone, BindCaptureStatement pstmt, int maxStringSize) { + super(dataTimeZone, maxStringSize, pstmt, null); this.captureStatement = pstmt; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java index ae0c10f4c1..d1ed0efdfd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java @@ -13,18 +13,20 @@ public final class CacheManagerOptions { private final ClusterManager clusterManager; - private final DatabaseConfig databaseConfig; + private final String serverName; private final boolean localL2Caching; private CurrentTenantProvider currentTenantProvider; private QueryCacheEntryValidate queryCacheEntryValidate; private ServerCacheFactory cacheFactory = new DefaultServerCacheFactory(); private ServerCacheOptions beanDefault = new ServerCacheOptions(); private ServerCacheOptions queryDefault = new ServerCacheOptions(); + private final boolean tenantPartitionedCache; CacheManagerOptions() { this.localL2Caching = true; this.clusterManager = null; - this.databaseConfig = null; + this.serverName = "db"; + this.tenantPartitionedCache = false; this.cacheFactory = new DefaultServerCacheFactory(); this.beanDefault = new ServerCacheOptions(); this.queryDefault = new ServerCacheOptions(); @@ -32,9 +34,10 @@ public final class CacheManagerOptions { public CacheManagerOptions(ClusterManager clusterManager, DatabaseConfig config, boolean localL2Caching) { this.clusterManager = clusterManager; - this.databaseConfig = config; + this.serverName = config.getName(); this.localL2Caching = localL2Caching; this.currentTenantProvider = config.getCurrentTenantProvider(); + this.tenantPartitionedCache = config.isTenantPartitionedCache(); } public CacheManagerOptions with(ServerCacheOptions beanDefault, ServerCacheOptions queryDefault) { @@ -55,7 +58,7 @@ public CacheManagerOptions with(CurrentTenantProvider currentTenantProvider) { } public String getServerName() { - return (databaseConfig == null) ? "db" : databaseConfig.getName(); + return serverName; } public boolean isLocalL2Caching() { @@ -85,4 +88,6 @@ public ClusterManager getClusterManager() { public QueryCacheEntryValidate getQueryCacheEntryValidate() { return queryCacheEntryValidate; } + + public boolean isTenantPartitionedCache() { return tenantPartitionedCache; } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java index aba5ab3638..06d322ecd1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java @@ -32,6 +32,7 @@ final class DefaultCacheHolder { private final ServerCacheOptions queryDefault; private final CurrentTenantProvider tenantProvider; private final QueryCacheEntryValidate queryCacheEntryValidate; + private final boolean tenantPartitionedCache; DefaultCacheHolder(CacheManagerOptions builder) { this.cacheFactory = builder.getCacheFactory(); @@ -39,6 +40,7 @@ final class DefaultCacheHolder { this.queryDefault = builder.getQueryDefault(); this.tenantProvider = builder.getCurrentTenantProvider(); this.queryCacheEntryValidate = builder.getQueryCacheEntryValidate(); + this.tenantPartitionedCache = builder.isTenantPartitionedCache(); } void visitMetrics(MetricVisitor visitor) { @@ -56,16 +58,42 @@ ServerCache getCache(Class beanType, String collectionProperty) { return getCacheInternal(beanType, ServerCacheType.COLLECTION_IDS, collectionProperty); } + private String key(String beanName) { + if (tenantPartitionedCache) { + StringBuilder sb = new StringBuilder(beanName.length() + 64); + sb.append(beanName); + sb.append('.'); + sb.append(tenantProvider.currentId()); + return sb.toString(); + } else { + return beanName; + } + } + private String key(String beanName, ServerCacheType type) { - return beanName + type.code(); + StringBuilder sb = new StringBuilder(beanName.length() + 64); + sb.append(beanName); + if (tenantPartitionedCache) { + sb.append('.'); + sb.append(tenantProvider.currentId()); + } + sb.append(type.code()); + return sb.toString(); } private String key(String beanName, String collectionProperty, ServerCacheType type) { + StringBuilder sb = new StringBuilder(beanName.length() + 64); + sb.append(beanName); + if (tenantPartitionedCache) { + sb.append('.'); + sb.append(tenantProvider.currentId()); + } if (collectionProperty != null) { - return beanName + "." + collectionProperty + type.code(); - } else { - return beanName + type.code(); + sb.append('.'); + sb.append(collectionProperty); } + sb.append(type.code()); + return sb.toString(); } /** @@ -82,12 +110,17 @@ private ServerCache createCache(Class beanType, ServerCacheType type, String if (type == ServerCacheType.COLLECTION_IDS) { lock.lock(); try { - collectIdCaches.computeIfAbsent(beanType.getName(), s -> new ConcurrentSkipListSet<>()).add(key); + collectIdCaches.computeIfAbsent(key(beanType.getName()), s -> new ConcurrentSkipListSet<>()).add(key); } finally { lock.unlock(); } } - return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate)); + if (tenantPartitionedCache) { + return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, null, queryCacheEntryValidate)); + } else { + return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate)); + } + } void clearAll() { @@ -103,7 +136,7 @@ public void clear(String name) { clearIfExists(key(name, ServerCacheType.QUERY)); clearIfExists(key(name, ServerCacheType.BEAN)); clearIfExists(key(name, ServerCacheType.NATURAL_KEY)); - Set keys = collectIdCaches.get(name); + Set keys = collectIdCaches.get(key(name)); if (keys != null) { for (String collectionIdKey : keys) { clearIfExists(collectionIdKey); @@ -147,4 +180,7 @@ private ServerCacheOptions getBeanOptions(Class cls) { return beanDefault.copy(nearCache); } + boolean isTenantPartitionedCache() { + return tenantPartitionedCache; + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java index 2590265716..a0eea215b2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java @@ -6,6 +6,7 @@ import io.ebean.cache.ServerCacheStatistics; import io.ebean.meta.MetricVisitor; import io.ebean.metric.CountMetric; +import io.ebean.metric.Metric; import io.ebean.metric.MetricFactory; import java.io.Serializable; @@ -48,6 +49,7 @@ public class DefaultServerCache implements ServerCache { protected final CountMetric idleCount; protected final CountMetric ttlCount; protected final CountMetric lruCount; + protected final Metric sizeCount; protected final String name; protected final String shortName; protected final int maxSize; @@ -81,6 +83,7 @@ public DefaultServerCache(DefaultServerCacheConfig config) { this.idleCount = factory.createCountMetric(prefix + shortName + ".idle"); this.ttlCount = factory.createCountMetric(prefix + shortName + ".ttl"); this.lruCount = factory.createCountMetric(prefix + shortName + ".lru"); + this.sizeCount = factory.createMetric(prefix + shortName + ".size", map::size); } @@ -103,6 +106,7 @@ public void visit(MetricVisitor visitor) { idleCount.visit(visitor); ttlCount.visit(visitor); lruCount.visit(visitor); + sizeCount.visit(visitor); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java index 0b7264ff90..39558d19d8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java @@ -154,4 +154,8 @@ public ServerCache getBeanCache(Class beanType) { return cacheHolder.getCache(beanType, ServerCacheType.BEAN); } + @Override + public boolean isTenantPartitionedCache() { + return cacheHolder.isTenantPartitionedCache(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java index bf6d4d5a21..ebeec00f68 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java @@ -88,4 +88,10 @@ public interface SpiCacheManager { */ void clearLocal(Class beanType); + /** + * returns true, if this chacheManager runs in tenant partitioned mode + * @return + */ + boolean isTenantPartitionedCache(); + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java index b4a0c5b202..245032121c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java @@ -126,7 +126,7 @@ private String limitOffset(String sql) { /** * Prepare and execute the SQL using the Binder. */ - public void executeSql(Binder binder, SpiQuery.Type type) throws SQLException { + public void executeSql(Binder binder) throws SQLException { startNano = System.nanoTime(); executeAsSql(binder); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/BeanRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/BeanRequest.java index b8224bc23b..d0a0afc495 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/BeanRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/BeanRequest.java @@ -123,4 +123,8 @@ public boolean logSummary() { public DataTimeZone dataTimeZone() { return server.dataTimeZone(); } + + public int maxStringSize() { + return server.maxStringSize(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java index c579807f9b..3fab82bd09 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java @@ -137,7 +137,7 @@ private void applyConfigServices(DatabaseConfig config) { private void checkMissingModulePathProvides() { URL servicesFile = ClassLoader.getSystemResource("META-INF/services/io.ebean.config.EntityClassRegister"); if (servicesFile != null) { - log.log(ERROR, "module-info.java is probably missing 'provides io.ebean.config.EntityClassRegister with EbeanEntityRegister' clause. EntityClassRegister exists but was not service loaded."); + log.log(ERROR, "module-info.java is probably missing ''provides io.ebean.config.EntityClassRegister with EbeanEntityRegister'' clause. EntityClassRegister exists but was not service loaded."); } } @@ -169,6 +169,7 @@ private BootupClasses bootupClasses(DatabaseConfig config) { bootup.addFindControllers(config.getFindControllers()); bootup.addPersistListeners(config.getPersistListeners()); bootup.addQueryAdapters(config.getQueryAdapters()); + bootup.addCustomDeployParser(config.getCustomDeployParsers()); bootup.addChangeLogInstances(config); return bootup; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java index b4a29697c3..a4b2aaaa77 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java @@ -45,6 +45,11 @@ public BasicMetricVisitor visitBasic() { return basic; } + @Override + public MetricReportGenerator createReportGenerator() { + return new HtmlMetricReportGenerator(server); + } + @Override public void resetAllMetrics() { server.visitMetrics(new ResetVisitor()); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultQueryPlanListener.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultQueryPlanListener.java index 0393240725..f38b9c8fab 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultQueryPlanListener.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultQueryPlanListener.java @@ -18,9 +18,9 @@ public void process(QueryPlanCapture capture) { // better to log this in JSON form? String dbName = capture.database().name(); for (MetaQueryPlan plan : capture.plans()) { - log.log(INFO, "queryPlan db:{0} label:{1} queryTimeMicros:{2} loc:{3} sql:{4} bind:{5} plan:{6}", - dbName, plan.label(), plan.queryTimeMicros(), plan.profileLocation(), - plan.sql(), plan.bind(), plan.plan()); + log.log(INFO, "queryPlan db:{0} label:{1} queryTimeMicros:{2} captureMicros:{3} whenCaptured:{4} captureCount:{5} loc:{6} sql:{7} bind:{8} plan:{9}", + dbName, plan.label(), plan.queryTimeMicros(), plan.captureMicros(), plan.whenCaptured(), plan.captureCount(), + plan.profileLocation(), plan.sql(), plan.bind(), plan.plan()); } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 98356f4186..b56b8e559c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -16,6 +16,8 @@ import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; import io.ebean.meta.*; +import io.ebean.metric.Metric; +import io.ebean.metric.MetricRegistry; import io.ebean.migration.auto.AutoMigrationRunner; import io.ebean.plugin.BeanType; import io.ebean.plugin.Plugin; @@ -79,9 +81,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { private final String serverName; private final DatabasePlatform databasePlatform; private final TransactionManager transactionManager; + private final TempFileProvider tempFileProvider; private final QueryPlanManager queryPlanManager; private final ExtraMetrics extraMetrics; private final DataTimeZone dataTimeZone; + private final int maxStringSize; private final ClockService clockService; private final CallOriginFactory callStackFactory; private final Persister persister; @@ -150,6 +154,7 @@ public DefaultServer(InternalConfiguration config, ServerCacheManager cache) { this.beanLoader = new DefaultBeanLoader(this); this.jsonContext = config.createJsonContext(this); this.dataTimeZone = config.getDataTimeZone(); + this.maxStringSize = config.getMaxStringSize(); this.clockService = config.getClockService(); DocStoreIntegration docStoreComponents = config.createDocStoreIntegration(this); @@ -158,6 +163,7 @@ public DefaultServer(InternalConfiguration config, ServerCacheManager cache) { this.queryPlanManager = config.initQueryPlanManager(transactionManager); this.metaInfoManager = new DefaultMetaInfoManager(this, this.config.getMetricNaming()); this.serverPlugins = config.getPlugins(); + this.tempFileProvider = config.getConfig().getTempFileProvider(); this.ddlGenerator = config.initDdlGenerator(this); this.scriptRunner = new DScriptRunner(this); @@ -242,6 +248,11 @@ public DataTimeZone dataTimeZone() { return dataTimeZone; } + @Override + public int maxStringSize() { + return maxStringSize; + } + @Override public MetaInfoManager metaInfo() { return metaInfoManager; @@ -373,6 +384,8 @@ public void shutdown(boolean shutdownDataSource, boolean deregisterDriver) { backgroundExecutor.shutdown(); // shutdown DataSource (if its an Ebean one) transactionManager.shutdown(shutdownDataSource, deregisterDriver); + tempFileProvider.shutdown(); + dumpMetrics(); shutdown = true; if (shutdownDataSource) { @@ -586,7 +599,11 @@ public void clearQueryStatistics() { */ @Override public T createEntityBean(Class type) { - return descriptor(type).createBean(); + final BeanDescriptor desc = descriptor(type); + if (desc == null) { + throw new IllegalArgumentException("No bean type " + type.getName() + " registered"); + } + return desc.createBean(); } /** @@ -815,6 +832,12 @@ public void merge(Object bean, MergeOptions options, @Nullable Transaction trans executeInTrans((txn) -> persister.merge(desc, checkEntityBean(bean), options, txn), transaction); } + @Override + public T mergeBeans(T bean, T existing, BeanMergeOptions options) { + BeanDescriptor desc = desc(bean.getClass()); + return (T) desc.mergeBeans(checkEntityBean(bean), (EntityBean) existing, options); + } + @Override public void lock(Object bean) { BeanDescriptor desc = desc(bean.getClass()); @@ -890,6 +913,7 @@ public DtoQuery createNamedDtoQuery(Class dtoType, String namedQuery) @Override public DtoQuery findDto(Class dtoType, SpiQuery ormQuery) { DtoBeanDescriptor descriptor = dtoBeanManager.getDescriptor(dtoType); + ormQuery.setDtoType(dtoType); return new DefaultDtoQuery<>(this, descriptor, ormQuery); } @@ -930,6 +954,18 @@ public T find(Class beanType, Object id, @Nullable Transaction transactio return findId(query, transaction); } + DtoQueryRequest createDtoQueryRequest(Type type, SpiDtoQuery query) { + SpiQuery ormQuery = query.getOrmQuery(); + if (ormQuery != null) { + ormQuery.setType(type); + ormQuery.setManualId(); + SpiOrmQueryRequest ormRequest = createQueryRequest(type, ormQuery, query.getTransaction()); + return new DtoQueryRequest<>(this, dtoQueryEngine, query, ormRequest); + } else { + return new DtoQueryRequest<>(this, dtoQueryEngine, query, null); + } + } + SpiOrmQueryRequest createQueryRequest(Type type, Query query, @Nullable Transaction transaction) { SpiOrmQueryRequest request = buildQueryRequest(type, query, transaction); request.prepareQuery(); @@ -967,40 +1003,6 @@ private SpiOrmQueryRequest buildQueryRequest(SpiQuery query, @Nullable return new OrmQueryRequest<>(this, queryEngine, query, (SpiTransaction) transaction); } - /** - * Try to get the object out of the persistence context. - */ - @Nullable - @SuppressWarnings("unchecked") - private T findIdCheckPersistenceContextAndCache(@Nullable Transaction transaction, SpiQuery query, Object id) { - SpiTransaction t = (SpiTransaction) transaction; - if (t == null) { - t = currentServerTransaction(); - } - BeanDescriptor desc = query.getBeanDescriptor(); - id = desc.convertId(id); - PersistenceContext pc = null; - if (t != null && useTransactionPersistenceContext(query)) { - // first look in the transaction scoped persistence context - pc = t.getPersistenceContext(); - if (pc != null) { - WithOption o = desc.contextGetWithOption(pc, id); - if (o != null) { - if (o.isDeleted()) { - // Bean was previously deleted in the same transaction / persistence context - return null; - } - return (T) o.getBean(); - } - } - } - if (!query.isBeanCacheGet() || (t != null && t.isSkipCache())) { - return null; - } - // Hit the L2 bean cache - return desc.cacheBeanGet(id, query.isReadOnly(), pc); - } - /** * Return true if transactions PersistenceContext should be used. */ @@ -1022,15 +1024,59 @@ public PersistenceContextScope persistenceContextScope(SpiQuery query) { private T findId(Query query, @Nullable Transaction transaction) { SpiQuery spiQuery = (SpiQuery) query; spiQuery.setType(Type.BEAN); + SpiOrmQueryRequest request = null; if (SpiQuery.Mode.NORMAL == spiQuery.getMode() && !spiQuery.isForceHitDatabase()) { // See if we can skip doing the fetch completely by getting the bean from the // persistence context or the bean cache - T bean = findIdCheckPersistenceContextAndCache(transaction, spiQuery, spiQuery.getId()); - if (bean != null) { - return bean; + SpiTransaction t = (SpiTransaction) transaction; + if (t == null) { + t = currentServerTransaction(); + } + BeanDescriptor desc = spiQuery.getBeanDescriptor(); + Object id = desc.convertId(spiQuery.getId()); + PersistenceContext pc = null; + if (t != null && useTransactionPersistenceContext(spiQuery)) { + // first look in the transaction scoped persistence context + pc = t.getPersistenceContext(); + if (pc != null) { + WithOption o = desc.contextGetWithOption(pc, id); + if (o != null) { + // We have found a hit. This could be also one with o.deleted() == true + // if bean was previously deleted in the same transaction / persistence context + return (T) o.getBean(); + } + } + } + if (t == null || !t.isSkipCache()) { + if (spiQuery.getUseQueryCache() != CacheMode.OFF) { + request = buildQueryRequest(spiQuery, transaction); + if (request.isQueryCacheActive()) { + // Hit the query cache + request.prepareQuery(); + T bean = request.getFromQueryCache(); + if (bean != null) { + return bean; + } + } + } + if (spiQuery.isBeanCacheGet()) { + // Hit the L2 bean cache + T bean = desc.cacheBeanGet(id, spiQuery.isReadOnly(), pc); + if (bean != null) { + if (request != null && request.isQueryCachePut()) { + // copy bean from the L2 cache to the faster query cache, if caching is enabled + request.prepareQuery(); + request.putToQueryCache(bean); + } + return bean; + } + } } } - SpiOrmQueryRequest request = buildQueryRequest(spiQuery, transaction); + + if (request == null) { + request = buildQueryRequest(spiQuery, transaction); + } request.prepareQuery(); if (request.isUseDocStore()) { return docStore().find(request); @@ -1080,15 +1126,18 @@ private T extractUnique(List list) { public Set findSet(Query query, Transaction transaction) { SpiOrmQueryRequest request = buildQueryRequest(Type.SET, query, transaction); request.resetBeanCacheAutoMode(false); + if (request.isQueryCacheActive()) { + request.prepareQuery(); + Object result = request.getFromQueryCache(); + if (result != null) { + return (Set) result; + } + } if ((transaction == null || !transaction.isSkipCache()) && request.getFromBeanCache()) { // hit bean cache and got all results from cache return request.beanCacheHitsAsSet(); } request.prepareQuery(); - Object result = request.getFromQueryCache(); - if (result != null) { - return (Set) result; - } try { request.initTransIfRequired(); return request.findSet(); @@ -1101,16 +1150,19 @@ public Set findSet(Query query, Transaction transaction) { @SuppressWarnings({"unchecked", "rawtypes"}) public Map findMap(Query query, @Nullable Transaction transaction) { SpiOrmQueryRequest request = buildQueryRequest(Type.MAP, query, transaction); + if (request.isQueryCacheActive()) { + request.prepareQuery(); + Object result = request.getFromQueryCache(); + if (result != null) { + return (Map) result; + } + } request.resetBeanCacheAutoMode(false); if ((transaction == null || !transaction.isSkipCache()) && request.getFromBeanCache()) { // hit bean cache and got all results from cache return request.beanCacheHitsAsMap(); } request.prepareQuery(); - Object result = request.getFromQueryCache(); - if (result != null) { - return (Map) result; - } try { request.initTransIfRequired(); return request.findMap(); @@ -1387,15 +1439,18 @@ public List findList(Query query, Transaction transaction) { private List findList(Query query, @Nullable Transaction transaction, boolean findOne) { SpiOrmQueryRequest request = buildQueryRequest(Type.LIST, query, transaction); request.resetBeanCacheAutoMode(findOne); + if (request.isQueryCacheActive()) { + request.prepareQuery(); + Object result = request.getFromQueryCache(); + if (result != null) { + return (List) result; + } + } if ((transaction == null || !transaction.isSkipCache()) && request.getFromBeanCache()) { // hit bean cache and got all results from cache return request.beanCacheHits(); } request.prepareQuery(); - Object result = request.getFromQueryCache(); - if (result != null) { - return (List) result; - } if (request.isUseDocStore()) { return docStore().findList(request); } @@ -1491,7 +1546,7 @@ public T findSingleAttribute(SpiSqlQuery query, Class cls) { @Override public void findDtoEach(SpiDtoQuery query, Consumer consumer) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query); try { request.initTransIfRequired(); request.findEach(consumer); @@ -1502,7 +1557,7 @@ public void findDtoEach(SpiDtoQuery query, Consumer consumer) { @Override public void findDtoEach(SpiDtoQuery query, int batch, Consumer> consumer) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query); try { request.initTransIfRequired(); request.findEach(batch, consumer); @@ -1513,7 +1568,7 @@ public void findDtoEach(SpiDtoQuery query, int batch, Consumer> c @Override public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query); try { request.initTransIfRequired(); request.findEachWhile(consumer); @@ -1524,7 +1579,7 @@ public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) { @Override public QueryIterator findDtoIterate(SpiDtoQuery query) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query); try { request.initTransIfRequired(); return request.findIterate(); @@ -1541,7 +1596,11 @@ public Stream findDtoStream(SpiDtoQuery query) { @Override public List findDtoList(SpiDtoQuery query) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.LIST, query); + List ret = request.getFromQueryCache(); + if (ret != null) { + return ret; + } try { request.initTransIfRequired(); return request.findList(); @@ -1553,13 +1612,7 @@ public List findDtoList(SpiDtoQuery query) { @Nullable @Override public T findDtoOne(SpiDtoQuery query) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); - try { - request.initTransIfRequired(); - return extractUnique(request.findList()); - } finally { - request.endTransIfRequired(); - } + return extractUnique(findDtoList(query)); } /** @@ -2155,11 +2208,11 @@ public void slowQueryCheck(long timeMicros, int rowCount, SpiQuery query) { @Override public Set checkUniqueness(Object bean) { - return checkUniqueness(bean, null); + return checkUniqueness(bean, null, false, false); } @Override - public Set checkUniqueness(Object bean, @Nullable Transaction transaction) { + public Set checkUniqueness(Object bean, @Nullable Transaction transaction, boolean useQueryCache, boolean skipClean) { EntityBean entityBean = checkEntityBean(bean); BeanDescriptor beanDesc = descriptor(entityBean.getClass()); BeanProperty idProperty = beanDesc.idProperty(); @@ -2167,17 +2220,18 @@ public Set checkUniqueness(Object bean, @Nullable Transaction transact if (idProperty == null) { return Collections.emptySet(); } - Object id = idProperty.value(entityBean); + Object id = idProperty.getValue(entityBean); if (entityBean._ebean_getIntercept().isNew() && id != null) { // Primary Key is changeable only on new models - so skip check if we are not new Query query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory); + query.setUseQueryCache(useQueryCache); query.setId(id); - if (findCount(query, transaction) > 0) { + if (exists(query, transaction)) { return Collections.singleton(idProperty); } } for (BeanProperty[] props : beanDesc.uniqueProps()) { - Set ret = checkUniqueness(entityBean, beanDesc, props, transaction); + Set ret = checkUniqueness(entityBean, beanDesc, props, transaction, useQueryCache, skipClean); if (ret != null) { return ret; } @@ -2185,26 +2239,47 @@ public Set checkUniqueness(Object bean, @Nullable Transaction transact return Collections.emptySet(); } + /** + * Checks, if any property is dirty. + */ + private boolean isAnyPropertyDirty(EntityBean entityBean, BeanProperty[] props) { + if (entityBean._ebean_getIntercept().isNew()) { + return true; + } + for (BeanProperty prop : props) { + if (entityBean._ebean_getIntercept().isDirtyProperty(prop.propertyIndex())) { + return true; + } + } + return false; + } + /** * Returns a set of properties if saving the bean will violate the unique constraints (defined by given properties). */ @Nullable - private Set checkUniqueness(EntityBean entityBean, BeanDescriptor beanDesc, BeanProperty[] props, @Nullable Transaction transaction) { + private Set checkUniqueness(EntityBean entityBean, BeanDescriptor beanDesc, BeanProperty[] props, @Nullable Transaction transaction, + boolean useQueryCache, boolean skipClean) { + if (skipClean && !isAnyPropertyDirty(entityBean, props)) { + return null; + } + BeanProperty idProperty = beanDesc.idProperty(); Query query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory); + query.setUseQueryCache(useQueryCache); ExpressionList exprList = query.where(); if (!entityBean._ebean_getIntercept().isNew()) { // if model is not new, exclude ourself. - exprList.ne(idProperty.name(), idProperty.value(entityBean)); + exprList.ne(idProperty.name(), idProperty.getValue(entityBean)); } - for (Property prop : props) { - Object value = prop.value(entityBean); + for (BeanProperty prop : props) { + Object value = prop.getValue(entityBean); if (value == null) { return null; } exprList.eq(prop.name(), value); } - if (findCount(query, transaction) > 0) { + if (exists(query, transaction)) { Set ret = new LinkedHashSet<>(); Collections.addAll(ret, props); return ret; @@ -2228,6 +2303,9 @@ public void visitMetrics(MetricVisitor visitor) { persister.visitMetrics(visitor); } extraMetrics.visitMetrics(visitor); + for (Metric metric : MetricRegistry.registered()) { + metric.visit(visitor); + } visitor.visitEnd(); } @@ -2246,4 +2324,9 @@ List queryPlanInit(QueryPlanInit initRequest) { List queryPlanCollectNow(QueryPlanRequest request) { return queryPlanManager.collect(request); } + + @Override + public void runDdl() { + ddlGenerator.runDdl(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java index 58c5dae77f..81e20f3c05 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java @@ -4,7 +4,6 @@ import io.ebean.core.type.DataReader; import io.ebeaninternal.api.SpiDtoQuery; import io.ebeaninternal.api.SpiEbeanServer; -import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.dto.DtoColumn; import io.ebeaninternal.server.dto.DtoMappingRequest; import io.ebeaninternal.server.dto.DtoQueryPlan; @@ -31,11 +30,13 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest { private final DtoQueryEngine queryEngine; private DtoQueryPlan plan; private DataReader dataReader; + private SpiOrmQueryRequest ormRequest; - DtoQueryRequest(SpiEbeanServer server, DtoQueryEngine engine, SpiDtoQuery query) { + DtoQueryRequest(SpiEbeanServer server, DtoQueryEngine engine, SpiDtoQuery query, SpiOrmQueryRequest ormRequest) { super(server, query, query.getTransaction()); this.queryEngine = engine; this.query = query; + this.ormRequest = ormRequest; query.obtainLocation(); } @@ -43,20 +44,16 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest { * Prepare and execute the SQL using the Binder. */ @Override - public void executeSql(Binder binder, SpiQuery.Type type) throws SQLException { + public void executeSql(Binder binder) throws SQLException { startNano = System.nanoTime(); - SpiQuery ormQuery = query.getOrmQuery(); - if (ormQuery != null) { - ormQuery.setType(type); - ormQuery.setManualId(); - - query.setCancelableQuery(ormQuery); + if (ormRequest != null) { // execute the underlying ORM query returning the ResultSet - SpiResultSet result = server.findResultSet(ormQuery, transaction); + query.setCancelableQuery(query.getOrmQuery()); + ormRequest.transaction(transaction); + SpiResultSet result = ormRequest.findResultSet(); this.pstmt = result.getStatement(); - this.sql = ormQuery.getGeneratedSql(); - setResultSet(result.getResultSet(), ormQuery.getQueryPlanKey()); - + this.sql = ormRequest.query().getGeneratedSql(); + setResultSet(result.getResultSet(), ormRequest.query().getQueryPlanKey()); } else { // native SQL query execution executeAsSql(binder); @@ -155,4 +152,18 @@ static String parseColumn(String columnLabel) { return columnLabel; } + public List getFromQueryCache() { + if (ormRequest != null) { + return ormRequest.getFromQueryCache(); + } else { + return null; + } + } + + public void putToQueryCache(List result) { + if (ormRequest != null && ormRequest.isQueryCachePut()) { + ormRequest.putToQueryCache(result); + } + } + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DumpMetrics.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DumpMetrics.java index c13a7d6dbe..4df3ce4ed9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DumpMetrics.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DumpMetrics.java @@ -7,6 +7,12 @@ import io.ebean.meta.SortMetric; import io.ebeaninternal.api.SpiEbeanServer; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.Comparator; import java.util.List; @@ -19,6 +25,7 @@ final class DumpMetrics { private boolean dumpHash; private boolean dumpSql; private boolean dumpLoc; + private boolean dumpHtml; private Comparator sortBy = SortMetric.NAME; @@ -32,6 +39,7 @@ final class DumpMetrics { dumpLoc = options.contains("loc"); dumpSql = options.contains("sql"); dumpHash = options.contains("hash"); + dumpHtml = options.contains("html"); for (int i = 5; i < 10; i++) { width = Math.max(width, optionWidth(i * 10)); } @@ -72,7 +80,16 @@ private Comparator setSortOption(String option) { } void dump() { - + if (dumpHtml) { + File file = new File("metric-report-" + server.name() + "-" + + DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss.SSS").format(LocalDateTime.now()) + ".html"); + try (OutputStream out = new FileOutputStream(file)) { + server.metaInfo().createReportGenerator().writeReport(out); + out("html report written to: " + file.getAbsolutePath() + "\n"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } out("-- Dumping metrics for " + server.name() + " -- "); ServerMetrics serverMetrics = server.metaInfo().collectMetrics(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/HtmlMetricReportGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/HtmlMetricReportGenerator.java new file mode 100644 index 0000000000..8f213f3e0f --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/HtmlMetricReportGenerator.java @@ -0,0 +1,665 @@ +package io.ebeaninternal.server.core; + +import io.ebean.Database; +import io.ebean.annotation.Platform; +import io.ebean.cache.ServerCacheManager; +import io.ebean.config.dbplatform.DatabasePlatform; +import io.ebean.core.type.ScalarTypeUtils; +import io.ebean.meta.*; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * HtmlMetricReportGenerator provides a neat interface for ebean metrics. + * + * @author Roland Praml, FOCONIS AG + */ +public class HtmlMetricReportGenerator implements MetricReportGenerator { + + private static final Pattern SPLITPATTERN = Pattern.compile("[\\._]"); + // pattern for MariaDB binary UUIDs + private static final Pattern BINPATTERN = Pattern.compile("\\[.*\\[(-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), " + + "(-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), " + + "(-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3})\\].*\\]"); + + private final DatabasePlatform platform; + private final MetaInfoManager metaInfo; + private final ServerCacheManager cacheManager; + private final QueryPlanRequest queryRequest = new QueryPlanRequest(); + private final QueryPlanInit initRequest = new QueryPlanInit(); + private final String name; + private List queryPlans = Collections.emptyList(); + + /** + * Initializes the report with some common defaults. + */ + public HtmlMetricReportGenerator(Database db) { + platform = db.pluginApi().databasePlatform(); + metaInfo = db.metaInfo(); + cacheManager = db.cacheManager(); + initRequest.thresholdMicros(100_000); + initRequest.setAll(true); + queryRequest.maxCount(10); + queryRequest.maxTimeMillis(30_000); + name = db.name(); + } + + /** + * This is used to configure the current report. + * It will receive the REST calls from the web application and returns either OK or RELOAD if the page should be reloaded. + */ + @Override + public synchronized String configure(List configurations) { + String ret = "OK"; + for (MetricReportValue configuration : configurations) { + + if (configuration.getName().startsWith("hash.")) { + String hash = configuration.getName().substring(5); + if (configuration.intValue() > 0) { + initRequest.hashes().add(hash); + } else { + initRequest.hashes().remove(hash); + } + if (initRequest.isAll()) { + initRequest.setAll(false); + ret = "REFRESH"; + } + + } else { + switch (configuration.getName()) { + case "queryRequest.maxCount": + queryRequest.maxCount(configuration.intValue()); + break; + + case "queryRequest.maxTimeMillis": + queryRequest.maxTimeMillis(configuration.intValue()); + break; + + case "queryRequest.since": + queryRequest.since(configuration.intValue()); + break; + + case "queryRequest.apply": + queryPlans = metaInfo.queryPlanCollectNow(queryRequest); + ret = "REFRESH"; + break; + + case "initRequest.thresholdMicros": + initRequest.thresholdMicros(configuration.intValue()); + break; + + case "initRequest.isAll": + if (configuration.intValue() > 0) { + initRequest.hashes().clear(); + } + initRequest.setAll(configuration.intValue() > 0); + break; + + case "initRequest.apply": + queryPlans = metaInfo.queryPlanInit(initRequest); + ret = "REFRESH"; + break; + + case "clearCaches": + cacheManager.clearAll(); + ret = "REFRESH"; + break; + + case "resetMetrics": + metaInfo.resetAllMetrics(); + ret = "REFRESH"; + break; + + default: + throw new IllegalArgumentException(configuration.getName() + " is invalid"); + + } + } + } + return ret; + } + + /** + * Writes the report as UTF-8 to the outputstream. + */ + @Override + public synchronized void writeReport(OutputStream out) throws IOException { + OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); + Html html = new Html(); + createTabs(html); + StringBuilder sb = new StringBuilder(60 + ); + sb.append("Ebean metrics report for "); + addText(sb, name); // prevent HTML injection in name ;) + html.write(writer, sb.toString()); + writer.flush(); + } + + /** + * Create tabs. Can be overwritten to create additional tabs. + */ + protected void createTabs(Html html) { + // by default, we want to collect all metrics, but do not want to reset them + BasicMetricVisitor bmv = new BasicMetricVisitor(name, MetricNamingMatch.INSTANCE, false, true, true, true); + metaInfo.visitMetrics(bmv); + actionTab(html); + queryMetricTab(bmv.queryMetrics(), html); + timedMetricTab(bmv.timedMetrics(), html); + countMetricTab(bmv.countMetrics(), html); + queryPlansTab(html); + } + + /** + * 1st tab: Action tab. + */ + protected void actionTab(Html html) { + HtmlTab tab; + + tab = html.tab("Actions"); + tab.startTable("Name", "Value"); + tab.input("initRequest.thresholdMicros", initRequest.thresholdMicros()); + tab.input("initRequest.isAll", initRequest.isAll() ? 1 : 0); + tab.action("initRequest.apply", "Start capturing"); + + tab.input("queryRequest.maxCount", queryRequest.maxCount()); + tab.input("queryRequest.maxTimeMillis", queryRequest.maxTimeMillis()); + tab.input("queryRequest.since", queryRequest.since()); + tab.tableRow("current time", System.currentTimeMillis()); + tab.action("queryRequest.apply", "Collect plans"); + + + tab.action("clearCaches", "Clear caches"); + tab.action("resetMetrics", "Reset all metrics"); + + tab.endTable(); + tab.html("

Usage:

"); + tab.html("
    \n"); + tab.html("
  1. Set initRequest.thresholdMicros to the value, you want to capture
  2. \n"); + tab.html("
  3. Add hashes or set initRequest.isAll = 1 (default)
  4. \n"); + tab.html("
  5. Click 'Start capturing'
  6. \n"); + tab.html("
  7. Do the action, which executes the query/queries you want to inspect
  8. \n"); + tab.html("
  9. Click 'Collect plans' to collect the query-plans
  10. \n"); + tab.html("
\n"); + } + + /** + * 2nd tab: Query Metrics. + */ + protected void queryMetricTab(List metrics, Html html) { + + HtmlTab tab = html.tab("Query metrics"); + tab.startTable("type?", "?", "?", "?", "count", "total", "mean", "max", "sql", "hash"); + for (MetaQueryMetric metric : metrics) { + String[] names = splitPad(metric.name(), 4); + tab.tableRow(names[0], names[1], names[2], names[3], + metric.count(), + micros(metric.total()), micros(metric.mean()), micros(metric.max()), + metric.sql(), hash(metric.hash())); + } + tab.endTable(); + } + + /** + * 3rd tab: Timed metrics. + */ + protected void timedMetricTab(List metrics, Html html) { + HtmlTab tab = html.tab("Timed metrics"); + tab.startTable("type?", "?", "?", "?", "count", "total", "mean", "max"); + for (MetaTimedMetric metric : metrics) { + String[] names = splitPad(metric.name(), 4); + tab.tableRow(names[0], names[1], names[2], names[3], + metric.count(), + micros(metric.total()), micros(metric.mean()), micros(metric.max())); + } + tab.endTable(); + } + + /** + * 4th tab: Count metrics. + */ + protected void countMetricTab(List metrics, Html html) { + HtmlTab tab = html.tab("Count Metrics"); + tab.startTable("type?", "?", "?", "?", "count"); + for (MetaCountMetric metric : metrics) { + String[] names = splitPad(metric.name(), 4); + tab.tableRow(names[0], names[1], names[2], names[3], metric.count()); + } + tab.endTable(); + } + + /** + * 5th tab: Query plans. + */ + protected void queryPlansTab(Html html) { + HtmlTab tab = html.tab("Query plans"); + tab.startTable("type?", "tenant?", "count", "micros", "sql", "whenCaptured", "captureMicros","hash"); + for (MetaQueryPlan queryPlan : queryPlans) { + tab.tableRow(queryPlan.beanType().getSimpleName(), + queryPlan.tenantId(), + queryPlan.captureCount(), + micros(queryPlan.queryTimeMicros()), + queryPlan(queryPlan.sql(), queryPlan.bind(), queryPlan.plan()), + queryPlan.whenCaptured(), + queryPlan.captureMicros(), + hash(queryPlan.hash())); + } + tab.endTable(); + } + + /** + * Splits the string on '.' or '_' with a fixed length of entries. + */ + protected static String[] splitPad(String name, int length) { + String[] ret = SPLITPATTERN.split(name, length); + if (ret.length < length) { + return Arrays.copyOf(ret, length); + } + return ret; + } + + /** + * Adds html-safe text to the string-builder. + */ + protected static void addText(StringBuilder sb, String text) { + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + + switch (ch) { + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '"': + sb.append("""); + break; + case '&': + sb.append("&"); + break; + case '\'': + sb.append("'"); + break; // HTML entity for Apostroph ' + default: + sb.append(ch); + break; + } + } + } + + /** + * Creates a QueryPlan object. This is rendered as sql and optionally displayable bind and plan. + */ + protected QueryPlan queryPlan(String sql, String bind, String plan) { + + if (bind != null && (platform.isPlatform(Platform.MARIADB) || platform.isPlatform(Platform.MYSQL))) { + // MariaDB UUIDs beatifier. + Matcher matcher = BINPATTERN.matcher(bind); + + while (matcher.matches()) { + byte[] bytes = new byte[16]; + for (int i = 0; i < 16; i++) { + bytes[i] = Byte.parseByte(matcher.group(i + 1)); + } + UUID uuid = ScalarTypeUtils.uuidFromBytes(bytes, true); + bind = bind.substring(0, matcher.start(1) - 1) + uuid + bind.substring(matcher.end(16) + 1); + matcher = BINPATTERN.matcher(bind); + } + } + return new QueryPlan(sql, bind, plan, platform.isPlatform(Platform.SQLSERVER)); + } + + /** + * Creates a Micros object. It is rendered in human readable form. e.g. "12345678" micros is converted to "12.3 s" + */ + protected Micros micros(long value) { + return new Micros(value); + } + + /** + * Creates a Hash object. It is rendered with a checkbox. + */ + protected Hash hash(String hash) { + return new Hash(hash, initRequest.includeHash(hash)); + } + + /** + * Returns the currently collected plans. Maybe useful, if you want to fetch them via JSON. + */ + public List getCurrentPlans() { + return queryPlans; + } + + /** + * Human readable micros object. + */ + protected static class Micros { + private final long micros; + + protected Micros(long micros) { + this.micros = micros; + } + + @Override + public String toString() { + if (micros < 1_000L) { + return String.format("%d µs", micros); + } else if (micros < 10_000L) { + return String.format("%.2f ms", micros / 1000d); + } else if (micros < 100_000L) { + return String.format("%.1f ms", micros / 1000d); + } else if (micros < 1_000_000L) { + return String.format("%.0f ms", micros / 1000d); + } else if (micros < 10_000_000L) { + return String.format("%.2f s", micros / 1000_000d); + } else if (micros < 100_000_000L) { + return String.format("%.1f s", micros / 1000_000d); + } else { + return String.format("%.0f s", micros / 1000_000d); + } + } + } + + /** + * QueryPan popup object. + */ + protected static class QueryPlan { + final String sql; + final String bind; + final String plan; + final boolean sqlServer; + + QueryPlan(String sql, String bind, String plan, boolean sqlServer) { + this.sql = sql; + this.bind = bind; + this.plan = plan; + this.sqlServer = sqlServer; + } + } + + /** + * Hash object. + */ + public static class Hash { + final String hash; + final boolean checked; + + Hash(String hash, boolean checked) { + this.hash = hash; + this.checked = checked; + } + + @Override + public String toString() { + return hash; + } + } + + /** + * This class reprensets the whole HTML plage with it's tabs. + */ + public static class Html { + + private final List tabs = new ArrayList<>(); + + public HtmlTab tab(String title) { + HtmlTab htmlTab = new HtmlTab(title); + tabs.add(htmlTab); + return htmlTab; + } + + protected void appendResource(Appendable out, String resName) throws IOException { + InputStream res = getClass().getResourceAsStream(resName); + if (res == null) { + throw new NullPointerException("Could not find " + resName); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(res, StandardCharsets.UTF_8))) { + Iterator it = reader.lines().iterator(); + while (it.hasNext()) { + out.append(it.next()).append('\n'); + } + } + } + + public void write(Appendable out, String title) throws IOException { + out.append("\n"); + out.append("\n"); + out.append(""); + out.append(title); + out.append(""); + //sb.append(""); + out.append(""); + out.append("\n"); + out.append(""); + out.append("

"); + out.append(title); + out.append("

"); + out.append(" display raw values
"); + out.append(" display query plan bind values
"); + out.append(" display query plan details
"); + out.append("
\n"); + // add radio inputs that controls the selected tabs. + for (int i = 0; i < tabs.size(); i++) { + out.append(" \n"); + } + + // add labels (=tabs) + out.append(" \n"); + + // add tab contents + out.append("
\n"); + for (int i = 0; i < tabs.size(); i++) { + out.append("
"); + out.append(tabs.get(i).toString()); + out.append("
\n"); + } + out.append("
\n"); + out.append("
\n"); + // add javascript. + out.append(""); + } + + /** + * Adds css for each tab by replacing the '@' sign with tab numbers. + */ + protected void addTabCss(Appendable sb, String template, String css) throws IOException { + for (int i = 0; i < tabs.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(template.replace("@", String.valueOf(i))); + } + sb.append(css); + sb.append('\n'); + } + } + + /** + * HtmlTab that is mainly used to render tables. + */ + public static class HtmlTab { + + private final String title; + + private final StringBuilder sb = new StringBuilder(); + + HtmlTab(String title) { + this.title = title; + } + + /** + * Return the table's title. + */ + public String getTitle() { + return title; + } + + /** + * Adds table headers. If header ends with '?' - a filter will be added. + */ + public void startTable(String... headers) { + sb.append("\n"); + sb.append("\n"); + sb.append(""); + + boolean hasFilter = false; + for (String header : headers) { + sb.append("\n"); + } + sb.append(""); + if (hasFilter) { + // add second header row for filters + sb.append(""); + for (String header : headers) { + sb.append("\n"); + } + sb.append(""); + } + sb.append("\n"); + } + + public void tableRow(Object... items) { + sb.append(""); + + for (Object item : items) { + if (item == null) { + sb.append(""); + + } else if (item instanceof QueryPlan) { + // Render the QueryPlan sql, bind and plan output. + sb.append(""); + + } else if (item instanceof Micros) { + sb.append(""); + + } else if (item instanceof Number) { + sb.append(""); + + } else if (item instanceof Hash) { + Hash hash = (Hash) item; + sb.append(""); + + } else { + sb.append(""); + + } + } + sb.append("\n"); + } + + /** + * Adds an input field to the table. Changes are sent to the updateValue javascript function. + */ + public void input(String label, long value) { + sb.append("\n"); + } + + /** + * Adds an action button to the table. Changes are sent to the updateValue javascript function. + */ + public void action(String actionId, String caption) { + sb.append(""); + sb.append("\n"); + } + + public void endTable() { + // add a second tbody. it is used as buffer for filtered entries + sb.append("
"); + addText(sb, header); + if (header.endsWith("?")) { + sb.setLength(sb.length() - 1); + hasFilter = true; + } + sb.append("
"); + if (header.endsWith("?")) { + sb.append("
 "); + QueryPlan queryPlan = (QueryPlan) item; + addText(sb, String.valueOf(queryPlan.sql)); + if (queryPlan.bind != null) { + sb.append("
"); + addText(sb, queryPlan.bind); + sb.append(""); + } + if (queryPlan.plan != null) { + sb.append(""); + if (queryPlan.sqlServer) { + sb.append("Download"); + } else { + addText(sb, String.valueOf(queryPlan.plan)); + } + sb.append(""); + } + sb.append("
"); + addText(sb, String.valueOf(item)); + sb.append("
"); + addText(sb, String.valueOf(((Micros) item).micros)); + sb.append("
"); + addText(sb, String.valueOf(item)); + sb.append(""); + sb.append(" ").append(hash.hash).append(""); + addText(sb, String.valueOf(item)); + sb.append("
"); + addText(sb, label); + sb.append(""); + sb.append("
 
\n"); + } + + /** + * Add HTML to this tab. + */ + public void html(String html) { + sb.append(html); + } + + @Override + public String toString() { + return sb.toString(); + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index 080d95560c..2d61e024c4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -4,10 +4,7 @@ import io.ebean.ExpressionFactory; import io.ebean.annotation.Platform; import io.ebean.cache.*; -import io.ebean.config.DatabaseConfig; -import io.ebean.config.ExternalTransactionManager; -import io.ebean.config.ProfilingConfig; -import io.ebean.config.SlowQueryListener; +import io.ebean.config.*; import io.ebean.config.dbplatform.DatabasePlatform; import io.ebean.config.dbplatform.DbHistorySupport; import io.ebean.event.changelog.ChangeLogListener; @@ -37,6 +34,7 @@ import io.ebeaninternal.server.expression.DefaultExpressionFactory; import io.ebeaninternal.server.expression.platform.DbExpressionHandler; import io.ebeaninternal.server.expression.platform.DbExpressionHandlerFactory; +import io.ebeaninternal.server.json.DJsonContext; import io.ebeaninternal.server.logger.DLogManager; import io.ebeaninternal.server.logger.DLoggerFactory; import io.ebeaninternal.server.persist.Binder; @@ -46,7 +44,6 @@ import io.ebeaninternal.server.query.*; import io.ebeaninternal.server.readaudit.DefaultReadAuditLogger; import io.ebeaninternal.server.readaudit.DefaultReadAuditPrepare; -import io.ebeaninternal.server.json.DJsonContext; import io.ebeaninternal.server.transaction.*; import io.ebeaninternal.server.type.DefaultTypeManager; import io.ebeaninternal.server.type.TypeManager; @@ -76,9 +73,11 @@ public final class InternalConfiguration { private final DatabasePlatform databasePlatform; private final DeployInherit deployInherit; private final TypeManager typeManager; + private final TempFileProvider tempFileProvider; private final DtoBeanManager dtoBeanManager; private final ClockService clockService; private final DataTimeZone dataTimeZone; + private final int maxStringSize; private final Binder binder; private final DeployCreateProperties deployCreateProperties; private final DeployUtil deployUtil; @@ -116,6 +115,7 @@ public final class InternalConfiguration { this.databasePlatform = config.getDatabasePlatform(); this.expressionFactory = initExpressionFactory(config); this.typeManager = new DefaultTypeManager(config, bootupClasses); + this.tempFileProvider = config.getTempFileProvider(); this.multiValueBind = createMultiValueBind(databasePlatform.platform()); this.deployInherit = new DeployInherit(bootupClasses); this.deployCreateProperties = new DeployCreateProperties(typeManager); @@ -130,7 +130,8 @@ public final class InternalConfiguration { Map draftTableMap = beanDescriptorManager.draftTableMap(); beanDescriptorManager.scheduleBackgroundTrim(); this.dataTimeZone = initDataTimeZone(); - this.binder = getBinder(typeManager, databasePlatform, dataTimeZone); + this.maxStringSize = config.getMaxStringSize(); + this.binder = getBinder(typeManager, databasePlatform, dataTimeZone, maxStringSize); this.cQueryEngine = new CQueryEngine(config, databasePlatform, binder, asOfTableMapping, draftTableMap); } @@ -266,15 +267,15 @@ ReadAuditPrepare getReadAuditPrepare() { /** * For 'As Of' queries return the number of bind variables per predicate. */ - private Binder getBinder(TypeManager typeManager, DatabasePlatform databasePlatform, DataTimeZone dataTimeZone) { + private Binder getBinder(TypeManager typeManager, DatabasePlatform databasePlatform, DataTimeZone dataTimeZone, int maxStringSize) { DbExpressionHandler jsonHandler = getDbExpressionHandler(databasePlatform); DbHistorySupport historySupport = databasePlatform.historySupport(); if (historySupport == null) { - return new Binder(typeManager, logManager, 0, false, jsonHandler, dataTimeZone, multiValueBind); + return new Binder(typeManager, logManager, 0, false, jsonHandler, dataTimeZone, maxStringSize, multiValueBind); } - return new Binder(typeManager, logManager, historySupport.getBindCount(), historySupport.isStandardsBased(), jsonHandler, dataTimeZone, multiValueBind); + return new Binder(typeManager, logManager, historySupport.getBindCount(), historySupport.isStandardsBased(), jsonHandler, dataTimeZone, maxStringSize, multiValueBind); } /** @@ -472,6 +473,10 @@ public DataTimeZone getDataTimeZone() { return dataTimeZone; } + public int getMaxStringSize() { + return maxStringSize; + } + public ServerCacheManager cacheManager() { return new DefaultCacheAdapter(cacheManager); } @@ -517,6 +522,10 @@ SpiLogManager getLogManager() { return logManager; } + public TempFileProvider getTempFileProvider() { + return tempFileProvider; + } + private ServerCachePlugin initServerCachePlugin() { if (config.isLocalOnlyL2Cache()) { localL2Caching = true; @@ -582,7 +591,8 @@ public QueryPlanManager initQueryPlanManager(TransactionManager transactionManag return QueryPlanManager.NOOP; } long threshold = config.getQueryPlanThresholdMicros(); - return new CQueryPlanManager(transactionManager, threshold, queryPlanLogger(databasePlatform.platform()), extraMetrics); + return new CQueryPlanManager(transactionManager, config.getCurrentTenantProvider(), + threshold, queryPlanLogger(databasePlatform.platform()), extraMetrics); } /** @@ -596,6 +606,8 @@ QueryPlanLogger queryPlanLogger(Platform platform) { return new QueryPlanLoggerSqlServer(); case ORACLE: return new QueryPlanLoggerOracle(); + case DB2: + return new QueryPlanLoggerDb2(config.getQueryPlanOptions()); default: return new QueryPlanLoggerExplain(); } @@ -616,6 +628,11 @@ private static class NoopDdl implements SpiDdlGenerator { this.ddlRun = ddlRun; } + @Override + public void runDdl() { + CoreLog.log.log(ERROR, "Manual DDL run not possible"); + } + @Override public void execute(boolean online) { if (online && ddlRun) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index 6f0930523c..b80601c72a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -40,11 +40,13 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery private PersistenceContext persistenceContext; private HashQuery cacheKey; private CQueryPlanKey queryPlanKey; + // The queryPlan during the request. + private CQueryPlan queryPlan; private SpiQuerySecondary secondaryQueries; private List cacheBeans; private BeanPropertyAssocMany manyProperty; private boolean inlineCountDistinct; - private Set dependentTables; + private boolean prepared; public OrmQueryRequest(SpiEbeanServer server, OrmQueryEngine queryEngine, SpiQuery query, SpiTransaction t) { super(server, t); @@ -68,6 +70,7 @@ public boolean isDeleteByStatement() { } else { // delete by ids due to cascading delete needs queryPlanKey = query.setDeleteByIdsPlan(); + queryPlan = null; return false; } } @@ -172,10 +175,24 @@ private void adapterPreQuery() { */ @Override public void prepareQuery() { - secondaryQueries = query.convertJoins(); - beanDescriptor.prepareQuery(query); - adapterPreQuery(); - queryPlanKey = query.prepare(this); + if (!prepared) { + secondaryQueries = query.convertJoins(); + beanDescriptor.prepareQuery(query); + adapterPreQuery(); + queryPlanKey = query.prepare(this); + prepared = true; + } + + } + + /** + * The queryPlanKey has to be updated, if elements are removed from an already prepared query. + */ + private void updateQueryPlanKey() { + if (prepared) { + queryPlanKey = query.prepare(this); + queryPlan = null; + } } public boolean isNativeSql() { @@ -470,7 +487,10 @@ public BeanPropertyAssocMany manyPropertyForOrderBy() { * query plan for this query exists. */ public CQueryPlan queryPlan() { - return beanDescriptor.queryPlan(queryPlanKey); + if (queryPlan == null) { + queryPlan = beanDescriptor.queryPlan(queryPlanKey); + } + return queryPlan; } /** @@ -488,6 +508,7 @@ public CQueryPlanKey queryPlanKey() { * Put the QueryPlan into the cache. */ public void putQueryPlan(CQueryPlan queryPlan) { + this.queryPlan = queryPlan; beanDescriptor.queryPlan(queryPlanKey, queryPlan); } @@ -496,8 +517,16 @@ public void resetBeanCacheAutoMode(boolean findOne) { query.resetBeanCacheAutoMode(findOne); } + @Override + public boolean isQueryCacheActive() { + return query.getUseQueryCache() != CacheMode.OFF + && (transaction == null || !transaction.isSkipCache()) + && !server.isDisableL2Cache(); + } + + @Override public boolean isQueryCachePut() { - return cacheKey != null && query.getUseQueryCache().isPut(); + return cacheKey != null && queryPlan != null && query.getUseQueryCache().isPut(); } public boolean isBeanCachePutMany() { @@ -606,7 +635,14 @@ public boolean getFromBeanCache() { BeanCacheResult cacheResult = beanDescriptor.cacheIdLookup(persistenceContext, idLookup.idValues()); // adjust the query (IN clause) based on the cache hits this.cacheBeans = idLookup.removeHits(cacheResult); - return idLookup.allHits(); + if (idLookup.allHits()) { + return true; + } else { + if (!this.cacheBeans.isEmpty()) { + updateQueryPlanKey(); + } + return false; + } } if (!beanDescriptor.isNaturalKeyCaching()) { return false; @@ -619,7 +655,14 @@ public boolean getFromBeanCache() { BeanCacheResult cacheResult = beanDescriptor.naturalKeyLookup(persistenceContext, naturalKeySet.keys()); // adjust the query (IN clause) based on the cache hits this.cacheBeans = data.removeHits(cacheResult); - return data.allHits(); + if (data.allHits()) { + return true; + } else { + if (!this.cacheBeans.isEmpty()) { + updateQueryPlanKey(); + } + return false; + } } } return false; @@ -631,13 +674,12 @@ public boolean getFromBeanCache() { @Override @SuppressWarnings("unchecked") public Object getFromQueryCache() { - if (query.getUseQueryCache() == CacheMode.OFF - || (transaction != null && transaction.isSkipCache()) - || server.isDisableL2Cache()) { + if (!isQueryCacheActive()) { return null; } else { cacheKey = query.queryHash(); } + // check if queryCache is active and put-only if (!query.getUseQueryCache().isGet()) { return null; } @@ -687,8 +729,9 @@ private boolean readAuditQueryType() { } } + @Override public void putToQueryCache(Object result) { - beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, dependentTables, transaction.getStartNanoTime())); + beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, queryPlan.dependentTables(), transaction.getStartNanoTime())); } /** @@ -758,15 +801,6 @@ public boolean isInlineCountDistinct() { return inlineCountDistinct; } - public void addDependentTables(Set tables) { - if (tables != null && !tables.isEmpty()) { - if (dependentTables == null) { - dependentTables = new LinkedHashSet<>(); - } - dependentTables.addAll(tables); - } - } - /** * Return true if no MaxRows or use LIMIT in SQL update. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java index f9b08aa5fb..d06369da8b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java @@ -272,24 +272,30 @@ private void onUpdateGeneratedProperties() { } else { // @WhenModified set without invoking interception Object oldVal = prop.getValue(entityBean); - Object value = generatedProperty.getUpdateValue(prop, entityBean, now()); - prop.setValueChanged(entityBean, value); - intercept.setOldValue(prop.propertyIndex(), oldVal); + if (transaction == null || transaction.isOverwriteGeneratedProperties() || oldVal == null) { // version handled above + Object value = generatedProperty.getUpdateValue(prop, entityBean, now()); + prop.setValueChanged(entityBean, value); + intercept.setOldValue(prop.propertyIndex(), oldVal); + } } } } private void onFailedUpdateUndoGeneratedProperties() { for (BeanProperty prop : beanDescriptor.propertiesGenUpdate()) { - Object oldVal = intercept.origValue(prop.propertyIndex()); - prop.setValue(entityBean, oldVal); + if (transaction == null || transaction.isOverwriteGeneratedProperties() || prop.isVersion()) { + Object oldVal = intercept.origValue(prop.propertyIndex()); + prop.setValue(entityBean, oldVal); + } } } private void onInsertGeneratedProperties() { for (BeanProperty prop : beanDescriptor.propertiesGenInsert()) { - Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now()); - prop.setValueChanged(entityBean, value); + if (transaction == null || transaction.isOverwriteGeneratedProperties() || prop.isVersion() || prop.getValue(entityBean) == null) { + Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now()); + prop.setValueChanged(entityBean, value); + } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java index ff2c0ef541..fdff9dc364 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java @@ -135,6 +135,21 @@ public interface SpiOrmQueryRequest extends BeanQueryRequest, DocQueryRequ */
A getFromQueryCache(); + /** + * Return if query cache is active. + */ + boolean isQueryCacheActive(); + + /** + * Return if results should be put to query cache. + */ + boolean isQueryCachePut(); + + /** + * Put the result to the query cache. + */ + void putToQueryCache(Object result); + /** * Maybe hit the bean cache returning true if everything was obtained from the * cache (that there were no misses). diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java index 9ab9ae4b5f..660644512e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java @@ -1,6 +1,7 @@ package io.ebeaninternal.server.core.bootup; import io.ebean.annotation.DocStore; +import io.ebean.bean.extend.EntityExtension; import io.ebean.config.DatabaseConfig; import io.ebean.config.IdGenerator; import io.ebean.config.ScalarTypeConverter; @@ -11,6 +12,7 @@ import io.ebean.event.changelog.ChangeLogRegister; import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; +import io.ebean.plugin.CustomDeployParser; import io.ebean.util.AnnotationUtil; import io.ebeaninternal.api.CoreLog; @@ -38,6 +40,7 @@ public class BootupClasses implements Predicate> { private final List> embeddableList = new ArrayList<>(); private final List> entityList = new ArrayList<>(); + private final List> entityExtensionList = new ArrayList<>(); private final List>> scalarTypeList = new ArrayList<>(); private final List>> scalarConverterList = new ArrayList<>(); private final List>> attributeConverterList = new ArrayList<>(); @@ -54,6 +57,7 @@ public class BootupClasses implements Predicate> { private final List> beanPersistListenerCandidates = new ArrayList<>(); private final List> beanQueryAdapterCandidates = new ArrayList<>(); private final List> serverConfigStartupCandidates = new ArrayList<>(); + private final List> customDeployParserCandidates = new ArrayList<>(); private final List idGeneratorInstances = new ArrayList<>(); private final List beanPersistControllerInstances = new ArrayList<>(); @@ -63,6 +67,7 @@ public class BootupClasses implements Predicate> { private final List beanPersistListenerInstances = new ArrayList<>(); private final List beanQueryAdapterInstances = new ArrayList<>(); private final List serverConfigStartupInstances = new ArrayList<>(); + private final List customDeployParserInstances = new ArrayList<>(); // single objects private Class changeLogPrepareClass; @@ -172,6 +177,10 @@ public void addServerConfigStartup(List startupInstances) { add(startupInstances, serverConfigStartupInstances, serverConfigStartupCandidates); } + public void addCustomDeployParser(List customDeployParser) { + add(customDeployParser, customDeployParserInstances, customDeployParserCandidates); + } + public void addChangeLogInstances(DatabaseConfig config) { readAuditPrepare = config.getReadAuditPrepare(); readAuditLogger = config.getReadAuditLogger(); @@ -288,6 +297,10 @@ public List getBeanQueryAdapters() { return createAdd(beanQueryAdapterInstances, beanQueryAdapterCandidates); } + public List getCustomDeployParsers() { + return createAdd(customDeployParserInstances, customDeployParserCandidates); + } + /** * Return the list of Embeddable classes. */ @@ -302,6 +315,13 @@ public List> getEntities() { return entityList; } + /** + * Return the list of entity extension classes. + */ + public List> getEntityExtensionList() { + return entityExtensionList; + } + /** * Return the list of ScalarTypes found. */ @@ -329,6 +349,8 @@ public boolean test(Class cls) { embeddableList.add(cls); } else if (isEntity(cls)) { entityList.add(cls); + } else if (isEntityExtension(cls)) { + entityExtensionList.add(cls); } else { return isInterestingInterface(cls); } @@ -408,6 +430,11 @@ private boolean isInterestingInterface(Class cls) { interesting = true; } + if (CustomDeployParser.class.isAssignableFrom(cls)) { + customDeployParserCandidates.add((Class) cls); + interesting = true; + } + // single instances, last assigned wins if (ChangeLogListener.class.isAssignableFrom(cls)) { changeLogListenerClass = (Class) cls; @@ -440,6 +467,10 @@ private boolean isEntity(Class cls) { return has(cls, Entity.class) || has(cls, Table.class) || has(cls, DocStore.class); } + private boolean isEntityExtension(Class cls) { + return has(cls, EntityExtension.class); + } + private boolean isEmbeddable(Class cls) { return has(cls, Embeddable.class); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java index b6cb6314ba..f161f18230 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java @@ -11,7 +11,7 @@ abstract class BaseCollectionHelp implements BeanCollectionHelp { final BeanPropertyAssocMany many; - private final BeanDescriptor targetDescriptor; + final BeanDescriptor targetDescriptor; final String propertyName; BeanCollectionLoader loader; @@ -21,12 +21,6 @@ abstract class BaseCollectionHelp implements BeanCollectionHelp { this.propertyName = many.name(); } - BaseCollectionHelp() { - this.many = null; - this.targetDescriptor = null; - this.propertyName = null; - } - @Override public final void setLoader(BeanCollectionLoader loader) { this.loader = loader; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java index 6532df7f15..35ff6003e0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java @@ -32,7 +32,7 @@ public static Collection getActualEntries(Object o) { if (o instanceof BeanCollection) { BeanCollection bc = (BeanCollection) o; if (!bc.isPopulated()) { - return null; + return bc.getLazyAddedEntries(true); } // For maps this is a collection of Map.Entry, otherwise it // returns a collection of beans diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 15d05881d4..468b9d5f8d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -319,7 +319,7 @@ public BeanDescriptor(BeanDescriptorMap owner, DeployBeanDescriptor deploy) { this.idOnlyReference = isIdOnlyReference(propertiesBaseScalar); boolean noRelationships = propertiesOne.length + propertiesMany.length == 0; this.cacheSharableBeans = noRelationships && deploy.getCacheOptions().isReadOnly(); - this.cacheHelp = new BeanDescriptorCacheHelp<>(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported); + this.cacheHelp = BeanDescriptorCacheHelpPartitioned.create(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported); this.jsonHelp = initJsonHelp(); this.draftHelp = new BeanDescriptorDraftHelp<>(this); this.docStoreAdapter = owner.createDocStoreBeanAdapter(this, deploy); @@ -708,22 +708,15 @@ public void metricPersistNoBatch(PersistRequest.Type type, long startNanos) { iudMetrics.addNoBatch(type, startNanos); } - public void merge(EntityBean bean, EntityBean existing) { - EntityBeanIntercept fromEbi = bean._ebean_getIntercept(); - EntityBeanIntercept toEbi = existing._ebean_getIntercept(); - int propertyLength = toEbi.propertyLength(); - String[] names = properties(); - for (int i = 0; i < propertyLength; i++) { - if (fromEbi.isLoadedProperty(i)) { - BeanProperty property = beanProperty(names[i]); - if (!toEbi.isLoadedProperty(i)) { - Object val = property.getValue(bean); - property.setValue(existing, val); - } else if (property.isMany()) { - property.merge(bean, existing); - } - } + /** + * Copies all modified fields from bean to existing. + * It returns normally the existing bean (or a new instance, if it was null) + */ + public EntityBean mergeBeans(EntityBean bean, EntityBean existing, BeanMergeOptions options) { + if (existing == null) { + existing = createEntityBean(); } + return new BeanMergeHelp(existing, options).mergeBeans(this, bean, existing); } /** @@ -1781,7 +1774,7 @@ public EntityBean createEntityBeanForJson() { /** * We actually need to do a query because we don't know the type without the discriminator value. */ - private T findReferenceBean(Object id, PersistenceContext pc) { + public T findReferenceBean(Object id, PersistenceContext pc) { DefaultOrmQuery query = new DefaultOrmQuery<>(this, ebeanServer, ebeanServer.expressionFactory()); query.setPersistenceContext(pc); return query.setId(id).findOne(); @@ -1932,13 +1925,6 @@ public BeanDescriptor descriptor(Class otherType) { return owner.descriptor(otherType); } - /** - * Returns true, if the table is managed (i.e. an existing m2m relation). - */ - public boolean isTableManaged(String tableName) { - return owner.isTableManaged(tableName); - } - /** * Return the order column property. */ @@ -2051,8 +2037,8 @@ public void contextPut(PersistenceContext pc, Object id, Object bean) { * Put the bean into the persistence context if it is absent. */ @Override - public Object contextPutIfAbsent(PersistenceContext pc, Object id, EntityBean localBean) { - return pc.putIfAbsent(rootBeanType, id, localBean); + public EntityBean contextPutIfAbsent(PersistenceContext pc, Object id, EntityBean localBean) { + return (EntityBean) pc.putIfAbsent(rootBeanType, id, localBean); } /** @@ -3398,12 +3384,13 @@ void jsonWriteProperties(SpiJsonWriter writeJson, EntityBean bean) { jsonHelp.jsonWriteProperties(writeJson, bean); } - public T jsonRead(SpiJsonReader jsonRead, String path, T target) throws IOException { - return jsonHelp.jsonRead(jsonRead, path, true, target); + public T jsonRead(SpiJsonReader jsonRead, String path) throws IOException { + return jsonHelp.jsonRead(jsonRead, path, true); } - T jsonReadObject(SpiJsonReader jsonRead, String path, T target) throws IOException { - return jsonHelp.jsonRead(jsonRead, path, false, target); + + T jsonReadObject(SpiJsonReader jsonRead, String path) throws IOException { + return jsonHelp.jsonRead(jsonRead, path, false); } public List uniqueProps() { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java index 7d3e64eb31..66ef306059 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -25,7 +25,7 @@ * * @param The entity bean type */ -final class BeanDescriptorCacheHelp { +abstract class BeanDescriptorCacheHelp { private static final System.Logger log = CoreLog.internal; @@ -34,7 +34,7 @@ final class BeanDescriptorCacheHelp { private static final System.Logger manyLog = AppLog.getLogger("io.ebean.cache.COLL"); private static final System.Logger natLog = AppLog.getLogger("io.ebean.cache.NATKEY"); - private final BeanDescriptor desc; + final BeanDescriptor desc; private final SpiCacheManager cacheManager; private final CacheOptions cacheOptions; /** @@ -42,13 +42,10 @@ final class BeanDescriptorCacheHelp { */ private final boolean cacheSharableBeans; private final boolean invalidateQueryCache; - private final Class beanType; + final Class beanType; private final String cacheName; private final BeanPropertyAssocOne[] propertiesOneImported; private final String[] naturalKey; - private final ServerCache beanCache; - private final ServerCache naturalKeyCache; - private final ServerCache queryCache; private final boolean noCaching; private final SpiCacheControl cacheControl; private final SpiCacheRegion cacheRegion; @@ -61,6 +58,15 @@ final class BeanDescriptorCacheHelp { */ private boolean cacheNotifyOnDelete; + static BeanDescriptorCacheHelp create(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, + boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { + if ((cacheOptions.isEnableQueryCache() || cacheOptions.isEnableBeanCache()) && cacheManager.isTenantPartitionedCache()) { + return new BeanDescriptorCacheHelpPartitioned<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + } else { + return new BeanDescriptorCacheHelpFixed<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + } + } + BeanDescriptorCacheHelp(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { this.desc = desc; @@ -72,38 +78,38 @@ final class BeanDescriptorCacheHelp { this.cacheSharableBeans = cacheSharableBeans; this.propertiesOneImported = propertiesOneImported; this.naturalKey = cacheOptions.getNaturalKey(); - if (!cacheOptions.isEnableQueryCache()) { - this.queryCache = null; - } else { - this.queryCache = cacheManager.getQueryCache(beanType); - } - if (cacheOptions.isEnableBeanCache()) { - this.beanCache = cacheManager.getBeanCache(beanType); - if (cacheOptions.getNaturalKey() != null) { - this.naturalKeyCache = cacheManager.getNaturalKeyCache(beanType); - } else { - this.naturalKeyCache = null; - } - } else { - this.beanCache = null; - this.naturalKeyCache = null; - } - this.noCaching = (beanCache == null && queryCache == null); + this.noCaching = !cacheOptions.isEnableQueryCache() && !cacheOptions.isEnableBeanCache(); if (noCaching) { this.cacheControl = DCacheControlNone.INSTANCE; this.cacheRegion = (invalidateQueryCache) ? cacheManager.getRegion(cacheOptions.getRegion()) : DCacheRegionNone.INSTANCE; } else { this.cacheRegion = cacheManager.getRegion(cacheOptions.getRegion()); - this.cacheControl = new DCacheControl(cacheRegion, (beanCache != null), (naturalKeyCache != null), (queryCache != null)); + this.cacheControl = new DCacheControl(cacheRegion, + cacheOptions.isEnableBeanCache(), + cacheOptions.isEnableBeanCache() && cacheOptions.getNaturalKey() != null, + cacheOptions.isEnableQueryCache()); } } + abstract boolean hasBeanCache(); + + abstract boolean hasQueryCache(); + + abstract ServerCache getQueryCache(); + + abstract ServerCache getNaturalKeyCache(); + + abstract ServerCache getBeanCache(); + + /** * Derive the cache notify flags. */ void deriveNotifyFlags() { - cacheNotifyOnAll = (invalidateQueryCache || beanCache != null || queryCache != null); + cacheNotifyOnAll = (invalidateQueryCache + || hasBeanCache() + || hasQueryCache()); cacheNotifyOnDelete = !cacheNotifyOnAll && isNotifyOnDeletes(); if (log.isLoggable(DEBUG)) { if (cacheNotifyOnAll || cacheNotifyOnDelete) { @@ -178,11 +184,11 @@ CacheOptions getCacheOptions() { * Clear the query cache. */ void queryCacheClear() { - if (queryCache != null) { + if (hasQueryCache()) { if (queryLog.isLoggable(DEBUG)) { queryLog.log(DEBUG, " CLEAR {0}", cacheName); } - queryCache.clear(); + getQueryCache().clear(); } } @@ -190,7 +196,7 @@ void queryCacheClear() { * Add query cache clear to the changeSet. */ private void queryCacheClear(CacheChangeSet changeSet) { - if (queryCache != null) { + if (hasQueryCache()) { changeSet.addClearQuery(desc); } } @@ -199,10 +205,10 @@ private void queryCacheClear(CacheChangeSet changeSet) { * Get a query result from the query cache. */ Object queryCacheGet(Object id) { - if (queryCache == null) { + if (!hasQueryCache()) { throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)"); } - Object queryResult = queryCache.get(id); + Object queryResult = getQueryCache().get(id); if (queryLog.isLoggable(DEBUG)) { if (queryResult == null) { queryLog.log(DEBUG, " GET {0}({1}) - cache miss", cacheName, id); @@ -217,13 +223,13 @@ Object queryCacheGet(Object id) { * Put a query result into the query cache. */ void queryCachePut(Object id, QueryCacheEntry entry) { - if (queryCache == null) { + if (!hasQueryCache()) { throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)"); } if (queryLog.isLoggable(DEBUG)) { queryLog.log(DEBUG, " PUT {0}({1})", cacheName, id); } - queryCache.put(id, entry); + getQueryCache().put(id, entry); } void manyPropRemove(String propertyName, String parentKey) { @@ -294,7 +300,7 @@ boolean manyPropLoad(BeanPropertyAssocMany many, BeanCollection bc, String */ void manyPropPut(BeanPropertyAssocMany many, Object details, String parentKey) { if (many.isElementCollection()) { - CachedBeanData data = (CachedBeanData) beanCache.get(parentKey); + CachedBeanData data = (CachedBeanData) getBeanCache().get(parentKey); if (data != null) { try { // add as JSON to bean cache @@ -306,7 +312,7 @@ void manyPropPut(BeanPropertyAssocMany many, Object details, String parentKey if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " UPDATE {0}({1}) changes:{2}", cacheName, parentKey, changes); } - beanCache.put(parentKey, newData); + getBeanCache().put(parentKey, newData); } catch (IOException e) { log.log(ERROR, "Error updating L2 cache", e); } @@ -352,7 +358,7 @@ BeanCacheResult cacheIdLookup(PersistenceContext context, Collection ids) if (ids.isEmpty()) { return new BeanCacheResult<>(); } - Map beanDataMap = beanCache.getAll(keys); + Map beanDataMap = getBeanCache().getAll(keys); if (beanLog.isLoggable(TRACE)) { beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet()); } @@ -374,7 +380,7 @@ BeanCacheResult naturalKeyLookup(PersistenceContext context, Set keys } // naturalKey -> Id map - Map naturalKeyMap = naturalKeyCache.getAll(keys); + Map naturalKeyMap = getNaturalKeyCache().getAll(keys); if (natLog.isLoggable(TRACE)) { natLog.log(TRACE, " MLOOKUP {0}({1}) - hits:{2}", cacheName, keys, naturalKeyMap); } @@ -391,7 +397,7 @@ BeanCacheResult naturalKeyLookup(PersistenceContext context, Set keys } Set ids = new HashSet<>(naturalKeyMap.values()); - Map beanDataMap = beanCache.getAll(ids); + Map beanDataMap = getBeanCache().getAll(ids); if (beanLog.isLoggable(TRACE)) { beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet()); } @@ -422,25 +428,16 @@ private void setupContext(Object bean, PersistenceContext context) { desc.contextPut(context, id, bean); } - /** - * Return the beanCache creating it if necessary. - */ - private ServerCache getBeanCache() { - if (beanCache == null) { - throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation."); - } - return beanCache; - } /** * Clear the bean cache. */ void beanCacheClear() { - if (beanCache != null) { + if (hasBeanCache()) { if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " CLEAR {0}", cacheName); } - beanCache.clear(); + getBeanCache().clear(); } } @@ -516,7 +513,7 @@ void beanCachePutAllDirect(Collection beans) { if (natLog.isLoggable(DEBUG)) { natLog.log(DEBUG, " MPUT {0}({1}, {2})", cacheName, naturalKey, natKeys.keySet()); } - naturalKeyCache.putAll(natKeys); + getNaturalKeyCache().putAll(natKeys); } } @@ -536,7 +533,7 @@ void beanCachePutDirect(EntityBean bean) { if (natLog.isLoggable(DEBUG)) { natLog.log(DEBUG, " PUT {0}({1}, {2})", cacheName, naturalKey, key); } - naturalKeyCache.put(naturalKey, key); + getNaturalKeyCache().put(naturalKey, key); } } } @@ -679,11 +676,11 @@ EntityBean embeddedBeanLoadDirect(CachedBeanData data, PersistenceContext contex * Remove a bean from the cache given its Id. */ void beanCacheApplyInvalidate(Collection keys) { - if (beanCache != null) { + if (hasBeanCache()) { if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " MREMOVE {0}({1})", cacheName, keys); } - beanCache.removeAll(new HashSet<>(keys)); + getBeanCache().removeAll(new HashSet<>(keys)); } for (BeanPropertyAssocOne imported : propertiesOneImported) { imported.cacheClear(); @@ -767,7 +764,7 @@ void persistDeleteIds(Collection ids, CacheChangeSet changeSet) { changeSet.addInvalidate(desc); } else { queryCacheClear(changeSet); - if (beanCache != null) { + if (hasBeanCache()) { changeSet.addBeanRemoveMany(desc, ids); } cacheDeleteImported(true, null, changeSet); @@ -782,7 +779,7 @@ void persistDelete(Object id, PersistRequestBean deleteRequest, CacheChangeSe changeSet.addInvalidate(desc); } else { queryCacheClear(changeSet); - if (beanCache != null) { + if (hasBeanCache()) { changeSet.addBeanRemove(desc, id); } cacheDeleteImported(true, deleteRequest.entityBean(), changeSet); @@ -817,7 +814,7 @@ void persistUpdate(Object id, PersistRequestBean updateRequest, CacheChangeSe } else { queryCacheClear(changeSet); - if (beanCache == null) { + if (!hasBeanCache()) { // query caching only return; } @@ -860,7 +857,7 @@ void persistTableIUD(TableIUD tableIUD, CacheChangeSet changeSet) { void cacheNaturalKeyPut(String key, String newKey) { if (newKey != null) { - naturalKeyCache.put(newKey, key); + getNaturalKeyCache().put(newKey, key); } } @@ -893,7 +890,7 @@ void cacheBeanUpdate(String key, Map changes, boolean updateNatu if (natLog.isLoggable(DEBUG)) { natLog.log(DEBUG, ".. update {0} REMOVE({1}) - old key for ({2})", cacheName, oldKey, key); } - naturalKeyCache.remove(oldKey); + getNaturalKeyCache().remove(oldKey); } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java new file mode 100644 index 0000000000..448f661cce --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java @@ -0,0 +1,72 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.cache.ServerCache; +import io.ebeaninternal.server.cache.SpiCacheManager; +import io.ebeaninternal.server.core.CacheOptions; + + +/** + * Helper for BeanDescriptor that manages the bean, query and collection caches. + * + * @param The entity bean type + */ +final class BeanDescriptorCacheHelpFixed extends BeanDescriptorCacheHelp { + private final ServerCache beanCache; + private final ServerCache naturalKeyCache; + private final ServerCache queryCache; + + + BeanDescriptorCacheHelpFixed(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, + boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { + super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + if (!cacheOptions.isEnableQueryCache()) { + this.queryCache = null; + } else { + this.queryCache = cacheManager.getQueryCache(beanType); + } + + if (cacheOptions.isEnableBeanCache()) { + this.beanCache = cacheManager.getBeanCache(beanType); + if (cacheOptions.getNaturalKey() != null) { + this.naturalKeyCache = cacheManager.getNaturalKeyCache(beanType); + } else { + this.naturalKeyCache = null; + } + } else { + this.beanCache = null; + this.naturalKeyCache = null; + } + } + + @Override + boolean hasBeanCache() { + return beanCache != null; + } + + @Override + boolean hasQueryCache() { + return queryCache != null; + } + + + @Override + ServerCache getQueryCache() { + return queryCache; + } + + @Override + ServerCache getNaturalKeyCache() { + return naturalKeyCache; + } + + /** + * Return the beanCache creating it if necessary. + */ + @Override + ServerCache getBeanCache() { + if (beanCache == null) { + throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation."); + } + return beanCache; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java new file mode 100644 index 0000000000..0e6ccd537f --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java @@ -0,0 +1,74 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.cache.ServerCache; +import io.ebeaninternal.server.cache.SpiCacheManager; +import io.ebeaninternal.server.core.CacheOptions; + +import java.util.function.Supplier; + +/** + * Helper for BeanDescriptor that manages the bean, query and collection caches. + * + * @param The entity bean type + */ +final class BeanDescriptorCacheHelpPartitioned extends BeanDescriptorCacheHelp { + private final Supplier beanCacheSupplier; + private final Supplier naturalKeyCacheSupplier; + private final Supplier queryCacheSupplier; + + + BeanDescriptorCacheHelpPartitioned(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, + boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { + super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + if (!cacheOptions.isEnableQueryCache()) { + this.queryCacheSupplier = null; + } else { + this.queryCacheSupplier = () -> cacheManager.getQueryCache(beanType); + } + + if (cacheOptions.isEnableBeanCache()) { + this.beanCacheSupplier = () -> cacheManager.getBeanCache(beanType); + if (cacheOptions.getNaturalKey() != null) { + this.naturalKeyCacheSupplier = () -> cacheManager.getNaturalKeyCache(beanType); + } else { + this.naturalKeyCacheSupplier = null; + } + } else { + this.beanCacheSupplier = null; + this.naturalKeyCacheSupplier = null; + } + } + + @Override + boolean hasBeanCache() { + return beanCacheSupplier != null; + } + + @Override + boolean hasQueryCache() { + return queryCacheSupplier != null; + } + + + @Override + ServerCache getQueryCache() { + return queryCacheSupplier.get(); + } + + @Override + ServerCache getNaturalKeyCache() { + return naturalKeyCacheSupplier.get(); + } + + /** + * Return the beanCache creating it if necessary. + */ + @Override + ServerCache getBeanCache() { + if (beanCacheSupplier != null) { + return beanCacheSupplier.get(); + } else { + throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation."); + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java index 71d79b9dce..6a4517fd72 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java @@ -61,13 +61,13 @@ public void jsonWriteElement(SpiJsonWriter ctx, Object element) { } @Override - public T jsonRead(SpiJsonReader jsonRead, String path, T target) throws IOException { - return readJsonElement(jsonRead, path, target); + public T jsonRead(SpiJsonReader jsonRead, String path) throws IOException { + return readJsonElement(jsonRead, path); } @SuppressWarnings("unchecked") - T readJsonElement(SpiJsonReader jsonRead, String path, T target) throws IOException { - return (T)targetDescriptor.jsonRead(jsonRead, path, target); + T readJsonElement(SpiJsonReader jsonRead, String path) throws IOException { + return (T)targetDescriptor.jsonRead(jsonRead, path); } void writeJsonElement(SpiJsonWriter ctx, Object element) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java index ecb283e5c2..2021a43cc9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java @@ -58,13 +58,13 @@ public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) } if (stringKey) { parser.nextToken(); - Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here? + Object val = readJsonElement(readJson, null); add.addKeyValue(fieldName, val); } else { parser.nextFieldName(); Object key = scalarTypeKey.jsonRead(parser); parser.nextFieldName(); - Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here? + Object val = readJsonElement(readJson, null); add.addKeyValue(key, val); } } while (true); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java index 720ef92caa..31c5e787b2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java @@ -65,7 +65,7 @@ void jsonWriteDirtyProperties(SpiJsonWriter writeJson, EntityBean bean, boolean[ } @SuppressWarnings("unchecked") - T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance, T target) throws IOException { + T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance) throws IOException { JsonParser parser = jsonRead.parser(); //noinspection StatementWithEmptyBody if (parser.getCurrentToken() == JsonToken.START_OBJECT) { @@ -82,7 +82,7 @@ T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance, T targe } if (desc.inheritInfo == null || !withInheritance) { - return jsonReadObject(jsonRead, path, target); + return jsonReadObject(jsonRead, path); } ObjectNode node = jsonRead.mapper().readTree(parser); @@ -97,25 +97,18 @@ T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance, T targe JsonNode discNode = node.get(discColumn); if (discNode == null || discNode.isNull()) { if (!desc.isAbstractType()) { - return desc.jsonReadObject(newReader, path, target); + return desc.jsonReadObject(newReader, path); } String msg = "Error reading inheritance discriminator - expected [" + discColumn + "] but no json key?"; throw new JsonParseException(newParser, msg, parser.getCurrentLocation()); } BeanDescriptor inheritDesc = (BeanDescriptor) inheritInfo.readType(discNode.asText()).desc(); - return inheritDesc.jsonReadObject(newReader, path, target); + return inheritDesc.jsonReadObject(newReader, path); } - private T jsonReadObject(SpiJsonReader readJson, String path, T target) throws IOException { - EntityBean bean; - if (target == null) { - bean = desc.createEntityBeanForJson(); - } else if (desc.beanType.isInstance(target)) { - bean = (EntityBean) target; - } else { - throw new ClassCastException(target.getClass().getName() + " provided, but " + desc.beanType.getClass().getName() + " expected"); - } + private T jsonReadObject(SpiJsonReader readJson, String path) throws IOException { + EntityBean bean = desc.createEntityBeanForJson(); return jsonReadProperties(readJson, bean, path); } @@ -133,12 +126,7 @@ private T jsonReadProperties(SpiJsonReader readJson, EntityBean bean, String pat String key = parser.getCurrentName(); BeanProperty p = desc.beanProperty(key); if (p != null) { - if (p.isVersion() && readJson.update() ) { - // skip version prop during update - p.jsonRead(readJson); - } else { - p.jsonRead(readJson, bean); - } + p.jsonRead(readJson, bean); } else { // read an unmapped property if (unmappedProperties == null) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 7778778ade..10604b2dde 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -75,6 +75,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy private final BeanFinderManager beanFinderManager; private final PersistListenerManager persistListenerManager; private final BeanQueryAdapterManager beanQueryAdapterManager; + private final CustomDeployParserManager customDeployParserManager; private final NamingConvention namingConvention; private final DeployCreateProperties createProperties; private final BeanManagerFactory beanManagerFactory; @@ -88,6 +89,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy private final BootupClasses bootupClasses; private final String serverName; private final List> elementDescriptors = new ArrayList<>(); + private final Set managedTables = new HashSet<>(); private final Map, BeanTable> beanTableMap = new HashMap<>(); private final Map> descMap = new HashMap<>(); private final Map> descQueueMap = new HashMap<>(); @@ -154,6 +156,7 @@ public BeanDescriptorManager(InternalConfiguration config) { this.persistListenerManager = new PersistListenerManager(bootupClasses); this.beanQueryAdapterManager = new BeanQueryAdapterManager(bootupClasses); this.beanFinderManager = new BeanFinderManager(bootupClasses); + this.customDeployParserManager = new CustomDeployParserManager(bootupClasses); this.transientProperties = new TransientProperties(); this.changeLogPrepare = config.changeLogPrepare(bootupClasses.getChangeLogPrepare()); this.changeLogListener = config.changeLogListener(bootupClasses.getChangeLogListener()); @@ -292,6 +295,7 @@ public Map deploy(List mappings) { readEntityBeanTable(); readEntityDeploymentAssociations(); readInheritedIdGenerators(); + deployInfoMap.values().forEach(customDeployParserManager::parse); // creates the BeanDescriptors readEntityRelationships(); List> list = new ArrayList<>(descMap.values()); @@ -410,8 +414,7 @@ public List> beanTypes(String tableName) { @Override public boolean isTableManaged(String tableName) { - return tableToDescMap.get(tableName.toLowerCase()) != null - || tableToViewDescMap.get(tableName.toLowerCase()) != null; + return managedTables.contains(tableName); } /** @@ -637,6 +640,14 @@ private void registerDescriptor(DeployBeanInfo info) { * BeanTables have all been created. */ private void readEntityDeploymentInitial() { + for (Class extensionClass : bootupClasses.getEntityExtensionList()) { + try { + // TODO: load class in an early stage + extensionClass.getField("_ebean_props").get(null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } for (Class entityClass : bootupClasses.getEntities()) { DeployBeanInfo info = createDeployBeanInfo(entityClass); deployInfoMap.put(entityClass, info); @@ -674,6 +685,9 @@ private void readEntityBeanTable() { for (DeployBeanInfo info : deployInfoMap.values()) { BeanTable beanTable = createBeanTable(info); beanTableMap.put(beanTable.getBeanType(), beanTable); + if (beanTable.getBaseTable() != null) { + managedTables.add(beanTable.getBaseTable()); + } } // register non-id embedded beans (after bean tables are created) for (DeployBeanInfo info : embeddedBeans) { @@ -768,7 +782,7 @@ private void secondaryPropsJoins(DeployBeanInfo info) { // find a join to that table... DeployBeanPropertyAssocOne assocOne = descriptor.findJoinToTable(tableName); if (assocOne == null) { - String msg = "Error with property " + prop.getFullBeanName() + ". Could not find a Relationship to table " + tableName + String msg = "Error with property " + prop+ ". Could not find a Relationship to table " + tableName + ". Perhaps you could use a @JoinColumn instead."; throw new RuntimeException(msg); } @@ -811,7 +825,7 @@ private DeployBeanDescriptor targetDescriptor(DeployBeanPropertyAssoc prop Class targetType = prop.getTargetType(); DeployBeanInfo info = deployInfoMap.get(targetType); if (info == null) { - throw new PersistenceException("Can not find descriptor [" + targetType + "] for " + prop.getFullBeanName()); + throw new PersistenceException("Can not find descriptor [" + targetType + "] for " + prop); } return info.getDescriptor(); } @@ -874,7 +888,7 @@ private boolean findMappedBy(DeployBeanPropertyAssocMany prop) { } } // multiple options so should specify mappedBy property - String msg = "Error on " + prop.getFullBeanName() + " missing mappedBy."; + String msg = "Error on " + prop + " missing mappedBy."; msg += " There are [" + matchSet.size() + "] possible properties in " + targetDesc; msg += " that this association could be mapped to. Please specify one using "; msg += "the mappedBy attribute on @OneToMany."; @@ -919,7 +933,7 @@ private void makeUnidirectional(DeployBeanPropertyAssocMany oneToMany) { if (!oneToMany.getCascadeInfo().isSave()) { // The property MUST have persist cascading so that inserts work. Class targetType = oneToMany.getTargetType(); - String msg = "Error on " + oneToMany.getFullBeanName() + ". @OneToMany MUST have "; + String msg = "Error on " + oneToMany + ". @OneToMany MUST have "; msg += "Cascade.PERSIST or Cascade.ALL because this is a unidirectional "; msg += "relationship. That is, there is no property of type " + owningType + " on " + targetType; throw new PersistenceException(msg); @@ -983,14 +997,14 @@ private void checkMappedByOneToOne(DeployBeanPropertyAssocOne prop) { private DeployBeanPropertyAssocOne mappedOneToOne(DeployBeanPropertyAssocOne prop, String mappedBy, DeployBeanDescriptor targetDesc) { DeployBeanProperty mappedProp = targetDesc.getBeanProperty(mappedBy); if (mappedProp == null) { - throw new PersistenceException("Error on " + prop.getFullBeanName() + " Can not find mappedBy property " + targetDesc + "." + mappedBy); + throw new PersistenceException("Error on " + prop + " Can not find mappedBy property " + targetDesc + "." + mappedBy); } if (!(mappedProp instanceof DeployBeanPropertyAssocOne)) { - throw new PersistenceException("Error on " + prop.getFullBeanName() + ". mappedBy property " + targetDesc + "." + mappedBy + " is not a OneToOne?"); + throw new PersistenceException("Error on " + prop + ". mappedBy property " + targetDesc + "." + mappedBy + " is not a OneToOne?"); } DeployBeanPropertyAssocOne mappedAssocOne = (DeployBeanPropertyAssocOne) mappedProp; if (!mappedAssocOne.isOneToOne()) { - throw new PersistenceException("Error on " + prop.getFullBeanName() + ". mappedBy property " + targetDesc + "." + mappedBy + " is not a OneToOne?"); + throw new PersistenceException("Error on " + prop + ". mappedBy property " + targetDesc + "." + mappedBy + " is not a OneToOne?"); } return mappedAssocOne; } @@ -1073,10 +1087,10 @@ private void checkMappedByOneToMany(DeployBeanInfo info, DeployBeanPropertyAs private DeployBeanPropertyAssocOne mappedManyToOne(DeployBeanPropertyAssocMany prop, DeployBeanDescriptor targetDesc, String mappedBy) { DeployBeanProperty mappedProp = targetDesc.getBeanProperty(mappedBy); if (mappedProp == null) { - throw new PersistenceException("Error on " + prop.getFullBeanName() + " Can not find mappedBy property " + mappedBy + " in " + targetDesc); + throw new PersistenceException("Error on " + prop + " Can not find mappedBy property " + mappedBy + " in " + targetDesc); } if (!(mappedProp instanceof DeployBeanPropertyAssocOne)) { - throw new PersistenceException("Error on " + prop.getFullBeanName() + ". mappedBy property " + mappedBy + " is not a ManyToOne? in " + targetDesc); + throw new PersistenceException("Error on " + prop + ". mappedBy property " + mappedBy + " is not a ManyToOne? in " + targetDesc); } return (DeployBeanPropertyAssocOne) mappedProp; } @@ -1124,15 +1138,15 @@ private void checkMappedByManyToMany(DeployBeanPropertyAssocMany prop) { private DeployBeanPropertyAssocMany mappedManyToMany(DeployBeanPropertyAssocMany prop, String mappedBy, DeployBeanDescriptor targetDesc) { DeployBeanProperty mappedProp = targetDesc.getBeanProperty(mappedBy); if (mappedProp == null) { - throw new PersistenceException("Error on " + prop.getFullBeanName() + " Can not find mappedBy property " + mappedBy + " in " + targetDesc); + throw new PersistenceException("Error on " + prop + " Can not find mappedBy property " + mappedBy + " in " + targetDesc); } if (!(mappedProp instanceof DeployBeanPropertyAssocMany)) { - throw new PersistenceException("Error on " + prop.getFullBeanName() + ". mappedBy property " + targetDesc + "." + mappedBy + " is not a ManyToMany?"); + throw new PersistenceException("Error on " + prop + ". mappedBy property " + targetDesc + "." + mappedBy + " is not a ManyToMany?"); } DeployBeanPropertyAssocMany mappedAssocMany = (DeployBeanPropertyAssocMany) mappedProp; if (!mappedAssocMany.isManyToMany()) { - throw new PersistenceException("Error on " + prop.getFullBeanName() + ". mappedBy property " + targetDesc + "." + mappedBy + " is not a ManyToMany?"); + throw new PersistenceException("Error on " + prop + ". mappedBy property " + targetDesc + "." + mappedBy + " is not a ManyToMany?"); } return mappedAssocMany; } @@ -1161,11 +1175,12 @@ private DeployBeanInfo createDeployBeanInfo(Class beanClass) { // set bean controller, finder and listener setBeanControllerFinderListener(desc); deplyInherit.process(desc); - desc.checkInheritanceMapping(); createProperties.createProperties(desc); DeployBeanInfo info = new DeployBeanInfo<>(deployUtil, desc); readAnnotations.readInitial(info); + + desc.checkInheritanceMapping(); return info; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanListHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanListHelp.java index c2ddfeca73..5e228bb9f9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanListHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanListHelp.java @@ -6,6 +6,7 @@ import io.ebean.bean.BeanCollectionAdd; import io.ebean.bean.EntityBean; import io.ebean.common.BeanList; +import io.ebean.common.BeanListLazyAdd; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.json.SpiJsonWriter; @@ -19,12 +20,11 @@ */ public class BeanListHelp extends BaseCollectionHelp { + private final boolean hasOrderColumn; + BeanListHelp(BeanPropertyAssocMany many) { super(many); - } - - BeanListHelp() { - super(); + hasOrderColumn = many.hasOrderColumn(); } @Override @@ -53,7 +53,12 @@ public final BeanCollection createEmptyNoParent() { @Override public final BeanCollection createEmpty(EntityBean parentBean) { - BeanList beanList = new BeanList<>(loader, parentBean, propertyName); + BeanList beanList; + if (hasOrderColumn) { + beanList = new BeanList<>(loader, parentBean, propertyName); + } else { + beanList = new BeanListLazyAdd<>(loader, parentBean, propertyName); + } if (many != null) { beanList.setModifyListening(many.modifyListenMode()); } @@ -62,7 +67,12 @@ public final BeanCollection createEmpty(EntityBean parentBean) { @Override public final BeanCollection createReference(EntityBean parentBean) { - BeanList beanList = new BeanList<>(loader, parentBean, propertyName); + BeanList beanList; + if (hasOrderColumn) { + beanList = new BeanList<>(loader, parentBean, propertyName); + } else { + beanList = new BeanListLazyAdd<>(loader, parentBean, propertyName); + } beanList.setModifyListening(many.modifyListenMode()); return beanList; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java index 6ce968ca2b..7652a1b214 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java @@ -20,18 +20,13 @@ */ public class BeanMapHelp extends BaseCollectionHelp { - private final BeanPropertyAssocMany many; - private final BeanDescriptor targetDescriptor; - private final String propertyName; private final BeanProperty beanProperty; /** * When help is attached to a specific many property. */ BeanMapHelp(BeanPropertyAssocMany many) { - this.many = many; - this.targetDescriptor = many.targetDescriptor(); - this.propertyName = many.name(); + super(many); this.beanProperty = targetDescriptor.beanProperty(many.mapKey()); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMergeHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMergeHelp.java new file mode 100644 index 0000000000..af187729cc --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMergeHelp.java @@ -0,0 +1,192 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.BeanMergeOptions; +import io.ebean.bean.EntityBean; +import io.ebean.bean.PersistenceContext; +import io.ebeaninternal.server.json.PathStack; +import io.ebeaninternal.server.transaction.DefaultPersistenceContext; + +import java.util.IdentityHashMap; +import java.util.Map; + +import static io.ebeaninternal.server.persist.DmlUtil.isNullOrZero; + +/** + * Helper class to control the merge process of two beans. + *

+ * It holds the persistence context used during merge and various options to fine tune the merge. + * + * @author Roland Praml, FOCONIS AG + */ +class BeanMergeHelp { + + private static final Object DUMMY = new Object(); + private final PersistenceContext persistenceContext; + private final Map processedBeans = new IdentityHashMap<>(); + private final PathStack pathStack; + private final boolean mergeId; + private final boolean mergeVersion; + private final boolean clearCollections; + private final boolean addExistingToPersistenceContext; + private final BeanMergeOptions.MergeHandler mergeHandler; + + + public BeanMergeHelp(EntityBean rootBean, BeanMergeOptions options) { + this.persistenceContext = extractPersistenceContext(rootBean, options); + if (options == null) { + this.mergeHandler = null; + this.pathStack = null; + this.mergeId = true; + this.mergeVersion = false; + this.clearCollections = true; + this.addExistingToPersistenceContext = true; + } else { + this.mergeHandler = options.getMergeHandler(); + this.pathStack = mergeHandler == null ? null : new PathStack(); + this.mergeId = options.isMergeId(); + this.mergeVersion = options.isMergeVersion(); + this.clearCollections = options.isClearCollections(); + this.addExistingToPersistenceContext = true; + } + } + + private PersistenceContext extractPersistenceContext(EntityBean rootBean, BeanMergeOptions options) { + PersistenceContext pc = options == null ? null : options.getPersistenceContext(); + if (pc == null && rootBean != null) { + pc = rootBean._ebean_getIntercept().persistenceContext(); + } + if (pc == null) { + pc = new DefaultPersistenceContext(); + } + return pc; + } + + /** + * Returns, if this bean is already processed. This is to avoid cycles. Internally we use an IdentityHasnMap. + */ + private boolean processed(EntityBean bean) { + return processedBeans.put(bean, DUMMY) != null; + } + + /** + * The persistence context. + */ + public PersistenceContext persistenceContext() { + return persistenceContext; + } + + /** + * Should we add existing beans to the current persistence-context? + */ + public boolean addExistingToPersistenceContext() { + return addExistingToPersistenceContext; + } + + /** + * Add a given bean to the persistence-context. Returns the bean from the PC, + * if it was already there. + */ + public EntityBean contextPutExisting(BeanDescriptor desc, EntityBean bean) { + if (bean == null || !addExistingToPersistenceContext) { + return null; + } + Object id = desc.id(bean); + if (!isNullOrZero(id)) { + return desc.contextPutIfAbsent(persistenceContext, id, bean); + } + return null; + } + + public EntityBean contextGet(BeanDescriptor desc, EntityBean bean) { + if (bean == null) { + return null; + } + Object id = desc.id(bean); + if (!isNullOrZero(id)) { + return (EntityBean) desc.contextGet(persistenceContext, id); + } + return null; + } + + /** + * Checks, if we shold merge that property. + */ + boolean checkMerge(BeanProperty property, EntityBean bean, EntityBean existing) { + if (!bean._ebean_getIntercept().isLoadedProperty(property.propertyIndex())) { + return false; + } + if (property.isId() && !mergeId) { + return false; + } + if (property.isVersion() && !mergeVersion) { + return false; + } + return mergeHandler == null || mergeHandler.mergeBeans(bean, existing, property, pathStack.peekWithNull()); + + } + + public void pushPath(String name) { + if (pathStack != null) { + pathStack.pushPathKey(name); + } + } + + public void popPath() { + if (pathStack != null) { + pathStack.pop(); + } + } + + public boolean clearCollections() { + return clearCollections; + } + + /** + * Merges two beans. Returns the merge result. This is + *

    + *
  • null if bean was null
  • + *
  • context bean if found in context + *
  • existing if existing war not null
  • + *
  • new instance if existing was null
  • + *
+ */ + public EntityBean mergeBeans(BeanDescriptor desc, EntityBean bean, EntityBean existing) { + if (bean == null) { + return null; + } + Object id = desc.id(bean); + if (!isNullOrZero(id)) { + EntityBean contextBean = (EntityBean) desc.contextGet(persistenceContext, id); + if (contextBean != null) { + existing = contextBean; + } + } + if (processed(bean)) { + return existing; + } + + if (desc.inheritInfo() != null) { + desc = desc.inheritInfo().readType(bean.getClass()).desc(); + } + + if (existing == null && !isNullOrZero(id)) { + if (desc.isReference(bean._ebean_getIntercept())) { + existing = (EntityBean) desc.createRef(id, persistenceContext); + } else { + // Now, we must hit the database and try to find the reference bean + existing = (EntityBean) desc.findReferenceBean(id, persistenceContext); + } + } + if (existing == null) { + existing = desc.createEntityBean(); + } + + for (BeanProperty prop : desc.propertiesAll()) { + if (checkMerge(prop, bean, existing)) { + prop.merge(bean, existing, this); + } + } + + return existing; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 16111e7bfa..66ddeb1e58 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -456,7 +456,8 @@ public int fetchPreference() { @Override public void appendFrom(DbSqlContext ctx, SqlJoinType joinType, String manyWhere) { if (formula && sqlFormulaJoin != null) { - ctx.appendFormulaJoin(sqlFormulaJoin, joinType, manyWhere); + String alias = ctx.tableAliasManyWhere(manyWhere); + ctx.appendFormulaJoin(sqlFormulaJoin, joinType, alias); } else if (secondaryTableJoin != null) { String relativePrefix = ctx.relativePrefix(secondaryTableJoinPrefix); secondaryTableJoin.addJoin(joinType, relativePrefix, ctx); @@ -883,6 +884,13 @@ public boolean isImportedPrimaryKey() { return importedPrimaryKey; } + /** + * If true, this property references O2O with its primary key. + */ + public boolean isPrimaryKeyExport() { + return false; + } + @Override public boolean isAssocMany() { // Returns false - override in BeanPropertyAssocMany. @@ -1014,7 +1022,7 @@ public List dbMigrationInfos() { /** * Return the bean Field associated with this property. */ - private Field field() { + public Field field() { return field; } @@ -1410,11 +1418,7 @@ private void jsonWriteScalar(SpiJsonWriter writeJson, Object value) throws IOExc public void jsonRead(SpiJsonReader ctx, EntityBean bean) throws IOException { Object objValue = jsonRead(ctx); if (jsonDeserialize) { - if (ctx.update()) { - setValueIntercept(bean, objValue); - } else { - setValue(bean, objValue); - } + setValue(bean, objValue); } } @@ -1479,8 +1483,16 @@ private boolean isKeywordType(DocPropertyType type, DocPropertyOptions docOption return type == DocPropertyType.TEXT && (docOptions.isCode() || id || discriminator); } - public void merge(EntityBean bean, EntityBean existing) { - // do nothing unless Many property + public void merge(EntityBean bean, EntityBean existing, BeanMergeHelp mergeHelp) { + if (isDiscriminator()) { + return; + } + Object val = getValue(bean); + if (val != null && scalarType != null && scalarType.mutable()) { + // for mutable types, we must "clone" the object. + val = scalarType.parse(scalarType.format(val)); + } + setValueIntercept(existing, val); } public void registerColumn(BeanDescriptor desc, String prefix) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 02246d6807..377055c9ce 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.ebean.SqlUpdate; import io.ebean.Transaction; import io.ebean.bean.BeanCollection; @@ -29,7 +27,6 @@ import java.util.*; import static java.lang.System.Logger.Level.ERROR; -import static java.lang.System.Logger.Level.WARNING; /** * Property mapped to a List Set or Map. @@ -41,9 +38,12 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST * Join for manyToMany intersection table. */ private final TableJoin intersectionJoin; + + private final boolean tableManaged; private final String intersectionPublishTable; private final String intersectionDraftTable; private final boolean orphanRemoval; + private final IntersectionFactoryHelp intersectionFactory; private IntersectionTable intersectionTable; /** * For ManyToMany this is the Inverse join used to build reference queries. @@ -102,10 +102,13 @@ public BeanPropertyAssocMany(BeanDescriptor descriptor, DeployBeanPropertyAss this.mapKey = deploy.getMapKey(); this.fetchOrderBy = deploy.getFetchOrderBy(); this.intersectionJoin = deploy.createIntersectionTableJoin(); + this.intersectionFactory = deploy.getIntersectionFactory(); if (intersectionJoin != null) { + this.tableManaged = deploy.isTableManaged(); this.intersectionPublishTable = intersectionJoin.getTable(); this.intersectionDraftTable = deploy.getIntersectionDraftTable(); } else { + this.tableManaged = false; this.intersectionPublishTable = null; this.intersectionDraftTable = null; } @@ -203,29 +206,54 @@ public void registerColumn(BeanDescriptor desc, String prefix) { */ @SuppressWarnings("rawtypes") public Collection rawCollection(EntityBean bean) { - return help.underlying(value(bean)); + return help.underlying(getValue(bean)); } /** * Copy collection value if existing is empty. */ @Override - public void merge(EntityBean bean, EntityBean existing) { - Object existingCollection = value(existing); - if (existingCollection instanceof BeanCollection) { - BeanCollection toBC = (BeanCollection) existingCollection; - if (!toBC.isPopulated()) { - Object fromCollection = value(bean); - if (fromCollection instanceof BeanCollection) { - BeanCollection fromBC = (BeanCollection) fromCollection; - if (fromBC.isPopulated()) { - toBC.loadFrom(fromBC); + public void merge(EntityBean bean, EntityBean existing, BeanMergeHelp mergeHelp) { + mergeHelp.pushPath(name); + Object fromCollection = beanCollection(bean); + Object existingCollection = getValue(existing); + if (fromCollection instanceof BeanCollection) { + BeanCollection fromBC = (BeanCollection) fromCollection; + + if (fromBC.isPopulated()) { + BeanCollection toBC; + if (existingCollection instanceof BeanCollection) { + toBC = (BeanCollection) existingCollection; + if (mergeHelp.addExistingToPersistenceContext()) { + for (Object detailBean : toBC.actualEntries(true)) { + mergeHelp.contextPutExisting(targetDescriptor, (EntityBean) detailBean); + } + } + if (mergeHelp.clearCollections()) { + toBC.clear(); + } + } else { + toBC = createEmpty(existing); + setValueIntercept(existing, toBC); + } + + for (Object detailBean : fromBC.actualDetails()) { + if (detailBean instanceof EntityBean) { + EntityBean toBean = mergeHelp.mergeBeans(targetDescriptor, (EntityBean) detailBean, null); + if (childMasterProperty != null) { + childMasterProperty.setValue(toBean, existing); + } + toBC.addBean(toBean); } } } + } else { + setValueIntercept(existing, fromCollection); } + mergeHelp.popPath(); } + /** * Add the bean to the appropriate collection on the parent bean. */ @@ -917,11 +945,11 @@ public boolean isIncludeCascadeSave() { // Note ManyToMany always included as we always 'save' // the relationship via insert/delete of intersection table // REMOVALS means including PrivateOwned relationships - return cascadeInfo.isSave() || hasJoinTable() || ModifyListenMode.REMOVALS == modifyListenMode; + return cascadeInfo.isSave() || (hasJoinTable() && !tableManaged) || ModifyListenMode.REMOVALS == modifyListenMode; } public boolean isIncludeCascadeDelete() { - return cascadeInfo.isDelete() || hasJoinTable() || ModifyListenMode.REMOVALS == modifyListenMode; + return cascadeInfo.isDelete() || (hasJoinTable() && !tableManaged) || ModifyListenMode.REMOVALS == modifyListenMode; } boolean isCascadeDeleteEscalate() { @@ -1006,7 +1034,7 @@ private Object jsonReadCollection(String json) throws IOException { if (JsonToken.VALUE_NULL == event) { return null; } - return jsonReadCollection(ctx, null, null); + return jsonReadCollection(ctx, null); } /** @@ -1019,16 +1047,15 @@ private void jsonWriteCollection(SpiJsonWriter ctx, String name, Object value, b /** * Read the collection (JSON Array) containing entity beans. */ - public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean, Object targets) throws IOException { + public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException { if (elementDescriptor != null && elementDescriptor.isJsonReadCollection()) { return elementDescriptor.jsonReadCollection(readJson, parentBean); } BeanCollection collection = createEmpty(parentBean); BeanCollectionAdd add = beanCollectionAdd(collection); - Map existingBeans = extractBeans(targets); do { - EntityBean detailBean = getDetailBean(readJson, existingBeans); + EntityBean detailBean = (EntityBean) targetDescriptor.jsonRead(readJson, name); if (detailBean == null) { // read the entire array break; @@ -1044,67 +1071,17 @@ public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean, } /** - * Find bean in the target collection and reuse it for JSON update. - */ - private EntityBean getDetailBean(SpiJsonReader readJson, Map targets) throws IOException { - BeanProperty idProperty = targetDescriptor.idProperty(); - if (targets == null || idProperty == null) { - return (EntityBean) targetDescriptor.jsonRead(readJson, name, null); - } else { - JsonToken token = readJson.parser().nextToken(); - if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) { - return null; - } - // extract the id. We have to buffer the JSON; - ObjectNode node = readJson.mapper().readTree(readJson.parser()); - SpiJsonReader jsonReader = readJson.forJson(node.traverse()); - JsonNode idNode = node.get(idProperty.name()); - Object id = idNode == null ? null : idProperty.jsonRead(readJson.forJson(idNode.traverse())); - return (EntityBean) targetDescriptor.jsonRead(jsonReader, name, targets.get(id)); - } - } - - /** - * Extract beans, that are currently in the target collection. (targets can be a List/Set/Map) + * Bind all the property values to the SqlUpdate. */ - private Map extractBeans(Object targets) { - Collection beans; - - if (targets == null) { - return null; - } else if (targets instanceof Map) { - if (((Map) targets).isEmpty()) { - return null; - } - beans = ((Map) targets).values(); - } else if (targets instanceof Collection) { - if (((Collection) targets).isEmpty()) { - return null; - } - beans = (Collection) targets; - } else { - CoreLog.log.log(WARNING, "Found non collection value " + targets.getClass().getSimpleName()); - return null; - } - - BeanProperty idProp = targetDescriptor.idProperty(); - Map ret = new HashMap<>(); - for (T bean : beans) { - if (bean instanceof EntityBean) { - Object id = idProp.getValue((EntityBean) bean); - if (id != null) { - ret.put(id, bean); - } - } - } - return ret.isEmpty() ? null : ret; + public void bindElementValue(SqlUpdate insert, Object value) { + targetDescriptor.bindElementValue(insert, value); } /** - * Bind all the property values to the SqlUpdate. + * Returns true, if this M2M beanproperty has a jointable, where the jointable is managed by an other entity. */ - public void bindElementValue(SqlUpdate insert, Object value) { - targetDescriptor.bindElementValue(insert, value); + public boolean isTableManaged() { + return tableManaged; } /** @@ -1113,9 +1090,14 @@ public void bindElementValue(SqlUpdate insert, Object value) { public boolean createJoinTable() { if (hasJoinTable() && mappedBy() == null) { // only create on other 'owning' side - return !descriptor.isTableManaged(intersectionJoin.getTable()); + return !tableManaged; } else { return false; } } + + public IntersectionFactoryHelp getIntersectionFactory() { + return intersectionFactory; + } + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java index d561f3e681..21573fe417 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java @@ -50,11 +50,7 @@ public void jsonRead(SpiJsonReader readJson, EntityBean parentBean) throws IOExc if (JsonToken.START_ARRAY != event && JsonToken.START_OBJECT != event) { throw new JsonParseException(parser, "Unexpected token " + event + " - expecting start array or object"); } - if (readJson.update()) { - many.setValueIntercept(parentBean, many.jsonReadCollection(readJson, parentBean, many.value(parentBean))); - } else { - many.setValue(parentBean, many.jsonReadCollection(readJson, parentBean, null)); - } + many.setValue(parentBean, many.jsonReadCollection(readJson, parentBean)); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index f44321d6f2..44742cbb52 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -30,10 +30,9 @@ import javax.persistence.PersistenceException; import java.io.IOException; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; + +import static io.ebeaninternal.server.persist.DmlUtil.isNullOrZero; /** * Property mapped to a joined bean. @@ -365,6 +364,11 @@ public boolean isOrphanRemoval() { return orphanRemoval; } + @Override + public boolean isPrimaryKeyExport() { + return primaryKeyExport; + } + @Override public void diff(String prefix, Map map, EntityBean newBean, EntityBean oldBean) { Object newEmb = (newBean == null) ? null : getValue(newBean); @@ -602,23 +606,42 @@ private ExportedProperty findMatch(boolean embeddedProp, BeanProperty prop) { return findMatch(embeddedProp, prop, prop.dbColumn(), tableJoin); } + /** + * If column is a primaryKeyExport colum, we can directly use our own ID and do not need to add a join if the relation is not optional + */ + boolean requiresJoin() { + return !primaryKeyExport || isNullable(); + } + @Override public void appendSelect(DbSqlContext ctx, boolean subQuery) { if (!isTransient) { - if (primaryKeyExport) { - descriptor.idProperty().appendSelect(ctx, subQuery); - } else { + if (requiresJoin()) { localHelp.appendSelect(ctx, subQuery); + } else { + descriptor.idProperty().appendSelect(ctx, subQuery); } } } + /** + * Add table join with explicit table alias. + */ + @Override + public SqlJoinType addJoin(SqlJoinType joinType, String a1, String a2, DbSqlContext ctx) { + if (sqlFormulaJoin != null) { + ctx.appendFormulaJoin(sqlFormulaJoin, joinType, a1); + } + return super.addJoin(joinType, a1, a2, ctx); + } + @Override public void appendFrom(DbSqlContext ctx, SqlJoinType joinType, String manyWhere) { - if (!isTransient && !primaryKeyExport) { + if (!isTransient && requiresJoin()) { localHelp.appendFrom(ctx, joinType); if (sqlFormulaJoin != null) { - ctx.appendFormulaJoin(sqlFormulaJoin, joinType, manyWhere); + String alias = ctx.tableAliasManyWhere(manyWhere); + ctx.appendFormulaJoin(sqlFormulaJoin, joinType, alias); } } } @@ -787,22 +810,11 @@ public void jsonWrite(SpiJsonWriter writeJson, EntityBean bean) throws IOExcepti @Override public void jsonRead(SpiJsonReader readJson, EntityBean bean) throws IOException { if (jsonDeserialize && targetDescriptor != null) { - // CHECKME: may we skip reading the object from the json stream? - T target = readJson.update() ? (T) value(bean) : null; - T assocBean = targetDescriptor.jsonRead(readJson, name, target); - if (readJson.update()) { - setValueIntercept(bean, assocBean); - } else { - setValue(bean, assocBean); - } + T assocBean = targetDescriptor.jsonRead(readJson, name); + setValue(bean, assocBean); } } - @Override - public Object jsonRead(SpiJsonReader readJson) throws IOException { - return targetDescriptor.jsonRead(readJson, name, null); - } - public boolean isReference(Object detailBean) { EntityBean eb = (EntityBean) detailBean; return targetDescriptor.isReference(eb._ebean_getIntercept()); @@ -825,6 +837,26 @@ public void setParentBeanToChild(EntityBean parent, EntityBean child) { } } + @Override + public void merge(EntityBean bean, EntityBean existing, BeanMergeHelp mergeHelp) { + mergeHelp.pushPath(name); + + EntityBean beanValue = valueAsEntityBean(bean); + EntityBean existingValue = valueAsEntityBean(existing); + + if (beanValue != null && existingValue != null) { + Object beanId = targetDescriptor.id(beanValue); + Object existingId = targetDescriptor.id(existingValue); + if (!isNullOrZero(beanId) && !isNullOrZero(existingId) && !Objects.equals(beanId, existingId)) { + existingValue = null; + } + } + + setValueIntercept(existing, mergeHelp.mergeBeans(targetDescriptor, beanValue, existingValue)); + + mergeHelp.popPath(); + } + public boolean hasCircularImportedId(BeanDescriptor sourceDesc) { return targetDescriptor.hasCircularImportedIdTo(sourceDesc); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java index dc170974d7..2fbf41dcc7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java @@ -19,7 +19,7 @@ public void bindElementValue(SqlUpdate insert, Object value) { } @Override - public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean, Object collectionValue) throws IOException { + public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException { return elementDescriptor.jsonReadCollection(readJson, parentBean); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanSetHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanSetHelp.java index ec2f6e31e1..4115b6ac4d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanSetHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanSetHelp.java @@ -26,13 +26,6 @@ public class BeanSetHelp extends BaseCollectionHelp { super(many); } - /** - * For a query that returns a set. - */ - BeanSetHelp() { - super(); - } - @Override public final BeanCollectionAdd getBeanCollectionAdd(Object bc, String mapKey) { if (bc instanceof BeanSet) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java new file mode 100644 index 0000000000..93a86d98b7 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java @@ -0,0 +1,23 @@ +package io.ebeaninternal.server.deploy; + +import java.util.List; + +import io.ebean.plugin.CustomDeployParser; +import io.ebeaninternal.server.core.bootup.BootupClasses; +import io.ebeaninternal.server.deploy.parse.DeployBeanInfo; + +public class CustomDeployParserManager { + + private final List parsers; + + public CustomDeployParserManager(BootupClasses bootupClasses) { + parsers = bootupClasses.getCustomDeployParsers(); + } + + public void parse(DeployBeanInfo value) { + for (CustomDeployParser parser : parsers) { + parser.parse(value.getDescriptor(), value.getUtil().dbPlatform()); + } + } + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java index 9557ee7a1e..be515b634a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java @@ -73,7 +73,7 @@ public interface DbSqlContext { * Append a Sql Formula join. This converts the "${ta}" keyword to the current * table alias. */ - void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType, String manyWhere); + void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType, String tableAlias); /** * Return the current content length. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionFactoryHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionFactoryHelp.java new file mode 100644 index 0000000000..e9bafa2890 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionFactoryHelp.java @@ -0,0 +1,66 @@ +package io.ebeaninternal.server.deploy; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * Helper class to construct intersection beans. + * + * @author Roland Praml, FOCONIS AG + */ +public class IntersectionFactoryHelp { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + private final MethodHandle handle; + + public IntersectionFactoryHelp(Class clazz, Class leftSide, Class rightSide, String factoryMethod) { + try { + if (factoryMethod.isEmpty()) { + Constructor ctor = findCtor(clazz, leftSide, rightSide); + handle = LOOKUP.findConstructor(clazz, MethodType.methodType(void.class, ctor.getParameterTypes())); + } else { + Method method = findMethod(clazz, factoryMethod, leftSide, rightSide); + handle = LOOKUP.findStatic(clazz, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes())); + } + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException("The factory" + clazz.getName() + + " must define a public constructor or static factory method that accepts (" + leftSide.getName() + ", " + rightSide.getName() + ")", e); + } + } + + private Constructor findCtor(Class clazz, Class leftSide, Class rightSide) throws NoSuchMethodException { + for (Constructor constructor : clazz.getConstructors()) { + Class[] types = constructor.getParameterTypes(); + if (types.length == 2) { + // we are only interested in ctors with 2 arguments + if (types[0].isAssignableFrom(leftSide) && types[1].isAssignableFrom(rightSide)) { + return constructor; + } + } + } + throw new NoSuchMethodException("Could not find valid constructor"); + } + + private Method findMethod(Class clazz, String methodName, Class leftSide, Class rightSide) throws NoSuchMethodException { + for (Method method : clazz.getMethods()) { + Class[] types = method.getParameterTypes(); + if (types.length == 2 && method.getName().equals(methodName)) { + // we are only interested in ctors with 2 arguments + if (types[0].isAssignableFrom(leftSide) && types[1].isAssignableFrom(rightSide)) { + return method; + } + } + } + throw new NoSuchMethodException("Could not find valid method"); + } + + public Object invoke(Object left, Object right) { + try { + return handle.invoke(left, right); + } catch (Throwable e) { + throw new RuntimeException("Unexpected error creating Intersection bean", e); + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java index d4021adeaa..6b8805073f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java @@ -65,7 +65,7 @@ public SpiSqlUpdate createInsert(SpiEbeanServer server) { return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) { + public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode, String extraWhere) { BindParams bindParams = new BindParams(); StringBuilder sb = new StringBuilder(); if (deleteMode.isHard()) { @@ -89,17 +89,33 @@ public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) { bindParams.setParameter(++count, bindValue); } } + addExtraWhere(sb, extraWhere); + return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SpiSqlUpdate createDeleteChildren(SpiEbeanServer server) { + public SpiSqlUpdate createDeleteChildren(SpiEbeanServer server, String extraWhere) { BindParams bindParams = new BindParams(); StringBuilder sb = new StringBuilder(); sb.append("delete from ").append(tableName).append(" where "); setBindParams(bindParams, sb); + addExtraWhere(sb, extraWhere); return new DefaultSqlUpdate(server, sb.toString(), bindParams); } + private void addExtraWhere(StringBuilder sb, String extraWhere) { + if (extraWhere != null) { + if (extraWhere.indexOf("${ta}") == -1) { + // no table alias append ${mta} to query. + sb.append(" and ").append(extraWhere.replace("${mta}", tableName)); + } else if (extraWhere.indexOf("${mta}") != -1) { + // we have a table alias - this is not interesting for deletion. + // but if have also a m2m table alias - this is a problem now! + throw new UnsupportedOperationException("extraWhere \'" + extraWhere + "\' has both ${ta} and ${mta} - this is not yet supported"); + } + } + } + private int setBindParams(BindParams bindParams, StringBuilder sb) { int count = 0; for (Map.Entry entry : values.entrySet()) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/BeanPropertyElementSetter.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/BeanPropertyElementSetter.java index c8a362b602..fcb34d0e07 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/BeanPropertyElementSetter.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/BeanPropertyElementSetter.java @@ -16,7 +16,7 @@ final class BeanPropertyElementSetter implements BeanPropertySetter { @Override public void set(EntityBean bean, Object value) { - bean._ebean_setField(pos, value); + bean._ebean_getIntercept().setValue(pos, value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index 97df0a16d4..0bd53816fa 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -10,6 +10,7 @@ import io.ebean.config.dbplatform.PlatformIdGenerator; import io.ebean.event.*; import io.ebean.event.changelog.ChangeLogFilter; +import io.ebean.plugin.DeployBeanDescriptorMeta; import io.ebean.text.PathProperties; import io.ebean.util.SplitName; import io.ebeaninternal.api.ConcurrencyMode; @@ -29,12 +30,19 @@ /** * Describes Beans including their deployment information. */ -public class DeployBeanDescriptor { +public class DeployBeanDescriptor implements DeployBeanDescriptorMeta { private static final Map EMPTY_NAMED_QUERY = new HashMap<>(); private static final Map EMPTY_RAW_MAP = new HashMap<>(); + /** + * Returns true, if the table is managed (i.e. an existing m2m relation). + */ + public boolean isTableManaged(String tableName) { + return manager.isTableManaged(tableName); + } + private static class PropOrder implements Comparator { @Override @@ -169,7 +177,7 @@ public TableJoin getPrimaryKeyJoin() { /** * Return the DeployBeanInfo for the given bean class. */ - DeployBeanInfo getDeploy(Class cls) { + public DeployBeanInfo getDeploy(Class cls) { return manager.deploy(cls); } @@ -597,6 +605,7 @@ public String[] getDependentTables() { * Return the base table. Only properties mapped to the base table are by * default persisted. */ + @Override public String getBaseTable() { return baseTable; } @@ -674,6 +683,7 @@ public Collection properties() { /** * Get a BeanProperty by its name. */ + @Override public DeployBeanProperty getBeanProperty(String propName) { return propMap.get(propName); } @@ -793,6 +803,7 @@ public String toString() { /** * Return a collection of all BeanProperty deployment information. */ + @Override public Collection propertiesAll() { return propMap.values(); } @@ -862,6 +873,7 @@ public String getSinglePrimaryKeyColumn() { /** * Return the BeanProperty that is the Id. */ + @Override public DeployBeanProperty idProperty() { if (idProperty != null) { return idProperty; @@ -1106,4 +1118,13 @@ private String getDeployWord(String expression) { } } + @Override + public String getDiscriminatorColumn() { + return inheritInfo == null ? null : inheritInfo.getDiscriminatorColumn(); + } + + @Override + public DeployBeanDescriptorMeta getDeployBeanDescriptorMeta(Class propertyType) { + return getDeploy(propertyType).getDescriptor(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index e31b76d169..4881c12afc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -6,6 +6,7 @@ import io.ebean.config.dbplatform.DbEncrypt; import io.ebean.config.dbplatform.DbEncryptFunction; import io.ebean.core.type.ScalarType; +import io.ebean.plugin.DeployBeanPropertyMeta; import io.ebean.util.AnnotationUtil; import io.ebeaninternal.server.core.InternString; import io.ebeaninternal.server.deploy.BeanProperty; @@ -35,7 +36,7 @@ * Description of a property of a bean. Includes its deployment information such * as database column mapping information. */ -public class DeployBeanProperty { +public class DeployBeanProperty implements DeployProperty, DeployBeanPropertyMeta { private static final int ID_ORDER = 1000000; private static final int UNIDIRECTIONAL_ORDER = 100000; @@ -222,14 +223,15 @@ private boolean isAuditProperty() { || AnnotationUtil.has(field, WhoCreated.class)); } - public String getFullBeanName() { - return desc.getFullName() + "." + name; - } - public DeployBeanDescriptor getDesc() { return desc; } + @Override + public Class getOwnerType() { + return desc.getBeanType(); + } + /** * Return the DB column length for character columns. *

@@ -262,10 +264,12 @@ public void setJsonDeserialize(boolean jsonDeserialize) { this.jsonDeserialize = jsonDeserialize; } + @Override public MutationDetection getMutationDetection() { return mutationDetection; } + @Override public void setMutationDetection(MutationDetection dirtyDetection) { this.mutationDetection = dirtyDetection; } @@ -404,6 +408,7 @@ public void setOwningType(Class owningType) { this.owningType = owningType; } + @Override public Class getOwningType() { return owningType; } @@ -432,6 +437,7 @@ public void setSetter(BeanPropertySetter setter) { /** * Return the name of the property. */ + @Override public String getName() { return name; } @@ -446,6 +452,7 @@ public void setName(String name) { /** * Return the bean Field associated with this property. */ + @Override public Field getField() { return field; } @@ -480,8 +487,9 @@ public void setGeneratedProperty(GeneratedProperty generatedValue) { } /** - * Return true if this property is mandatory. + * Return true if this property is not mandatory. */ + @Override public boolean isNullable() { return nullable; } @@ -549,6 +557,7 @@ public String getSqlFormulaJoin() { /** * The property is based on a formula. */ + @Override public void setSqlFormula(String formulaSelect, String formulaJoin) { this.sqlFormulaSelect = formulaSelect; this.sqlFormulaJoin = formulaJoin.isEmpty() ? null : formulaJoin; @@ -652,6 +661,7 @@ public String getElPlaceHolder() { /** * The database column name this is mapped to. */ + @Override public String getDbColumn() { if (sqlFormulaSelect != null) { return sqlFormulaSelect; @@ -845,6 +855,7 @@ public void setTransient() { /** * Return the property type. */ + @Override public Class getPropertyType() { return propertyType; } @@ -852,6 +863,7 @@ public Class getPropertyType() { /** * Return the generic type for this property. */ + @Override public Type getGenericType() { return genericType; } @@ -1056,6 +1068,7 @@ public A getMetaAnnotation(Class annotationType) { return null; } + @Override @SuppressWarnings("unchecked") public List getMetaAnnotations(Class annotationType) { List result = new ArrayList<>(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java index 3461be3bd8..123972cb0d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.deploy.meta; +import io.ebean.plugin.DeployBeanPropertyAssocMeta; import io.ebeaninternal.server.deploy.BeanCascadeInfo; import io.ebeaninternal.server.deploy.BeanTable; import io.ebeaninternal.server.deploy.PropertyForeignKey; @@ -7,7 +8,7 @@ /** * Abstract base for properties mapped to an associated bean, list, set or map. */ -public abstract class DeployBeanPropertyAssoc extends DeployBeanProperty { +public abstract class DeployBeanPropertyAssoc extends DeployBeanProperty implements DeployBeanPropertyAssocMeta { /** * The type of the joined bean. @@ -129,6 +130,7 @@ public PropertyForeignKey getForeignKey() { * this 'master' bean. *

*/ + @Override public String getMappedBy() { return mappedBy; } @@ -173,4 +175,9 @@ public void setFetchPreference(int fetchPreference) { public void setTargetType(Class targetType) { this.targetType = (Class)targetType; } + + @Override + public String getBaseTable() { + return getBeanTable().getBaseTable(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java index c201308e2d..45f61b77ee 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java @@ -2,6 +2,7 @@ import io.ebean.bean.BeanCollection.ModifyListenMode; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.deploy.IntersectionFactoryHelp; import io.ebeaninternal.server.deploy.ManyType; import io.ebeaninternal.server.deploy.TableJoin; import io.ebeaninternal.server.type.TypeReflectHelper; @@ -32,6 +33,12 @@ public class DeployBeanPropertyAssocMany extends DeployBeanPropertyAssoc { * Join for manyToMany intersection table. */ private DeployTableJoin intersectionJoin; + + /** + * Factory to create intersection beans (instead of rows). For managed intersections. + */ + private IntersectionFactoryHelp intersectionFactory; + /** * For ManyToMany this is the Inverse join used to build reference queries. */ @@ -113,6 +120,10 @@ public TableJoin createIntersectionTableJoin() { } } + public boolean isTableManaged() { + return intersectionJoin != null && desc.isTableManaged(intersectionJoin.getTable()); + } + /** * Create the immutable version of the inverse join. */ @@ -149,6 +160,20 @@ public void setInverseJoin(DeployTableJoin inverseJoin) { this.inverseJoin = inverseJoin; } + /** + * Return the intersection factory. + */ + public IntersectionFactoryHelp getIntersectionFactory() { + return intersectionFactory; + } + + /** + * Sets the intersection factory. + */ + public void setIntersectionFactory(IntersectionFactoryHelp intersectionFactory) { + this.intersectionFactory = intersectionFactory; + } + /** * Return the order by clause used to order the fetching of the data for * this list, set or map. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java new file mode 100644 index 0000000000..0fe3a4b8f1 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java @@ -0,0 +1,53 @@ +package io.ebeaninternal.server.deploy.meta; + +import io.ebean.annotation.MutationDetection; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Property, with basic type information (BeanProperty and DtoProperty). + */ +public interface DeployProperty { + + /** + * Return the name of the property. + */ + String getName(); + + /** + * Return the generic type for this property. + */ + Type getGenericType(); + + /** + * Return the property type. + */ + Class getPropertyType(); + + /** + * Returns the owner class of this property. + */ + Class getOwnerType(); + + /** + * Returns the annotations on this property. + */ + List getMetaAnnotations(Class annotationType); + + /** + * Returns the mutation detection setting of this property. + */ + MutationDetection getMutationDetection(); + + /** + * Sets the mutation detection setting of this property. + */ + void setMutationDetection(MutationDetection mutationDetection); + + /** + * Return true if this property is not mandatory. + */ + boolean isNullable(); +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssoc.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssoc.java index 191a43c40a..0176c31d4b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssoc.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssoc.java @@ -23,7 +23,7 @@ void setTargetType(Class targetType, DeployBeanPropertyAssoc prop) { void setBeanTable(DeployBeanPropertyAssoc prop) { BeanTable assoc = getBeanTable(prop); if (assoc == null) { - throw new BeanNotRegisteredException(errorMsgMissingBeanTable(prop.getTargetType(), prop.getFullBeanName())); + throw new BeanNotRegisteredException(errorMsgMissingBeanTable(prop.getTargetType(), prop.toString())); } prop.setBeanTable(assoc); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java index 0d29c3ba41..cfcfd1609a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java @@ -4,15 +4,13 @@ import io.ebean.annotation.FetchPreference; import io.ebean.annotation.HistoryExclude; import io.ebean.annotation.Where; +import io.ebean.annotation.ext.IntersectionFactory; import io.ebean.bean.BeanCollection.ModifyListenMode; import io.ebean.config.NamingConvention; import io.ebean.config.TableName; import io.ebean.core.type.ScalarType; import io.ebean.util.CamelCaseHelper; -import io.ebeaninternal.server.deploy.BeanDescriptorManager; -import io.ebeaninternal.server.deploy.BeanProperty; -import io.ebeaninternal.server.deploy.BeanTable; -import io.ebeaninternal.server.deploy.PropertyForeignKey; +import io.ebeaninternal.server.deploy.*; import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; @@ -128,6 +126,11 @@ private void read(DeployBeanPropertyAssocMany prop) { prop.getTableJoin().addJoinColumn(util, true, joinColumns, beanTable); } + IntersectionFactory intersectionFactory = get(prop, IntersectionFactory.class); + if (intersectionFactory != null) { + readIntersectionFactory(prop, intersectionFactory); + } + JoinTable joinTable = get(prop, JoinTable.class); if (joinTable != null) { if (prop.isManyToMany()) { @@ -165,9 +168,15 @@ private void read(DeployBeanPropertyAssocMany prop) { } } + private void readIntersectionFactory(DeployBeanPropertyAssocMany prop, IntersectionFactory factory) { + Class leftSide = descriptor.getBeanType(); + Class rightSide = prop.getPropertyType(); + prop.setIntersectionFactory(new IntersectionFactoryHelp(factory.value(), leftSide, rightSide, factory.factoryMethod())); + } + private void checkSelfManyToMany(DeployBeanPropertyAssocMany prop) { if (prop.getTargetType().equals(descriptor.getBeanType())) { - throw new IllegalStateException("@ManyToMany mapping for " + prop.getFullBeanName() + " requires explicit @JoinTable with joinColumns & inverseJoinColumns. Refer issue #2157"); + throw new IllegalStateException("@ManyToMany mapping for " + prop + " requires explicit @JoinTable with joinColumns & inverseJoinColumns. Refer issue #2157"); } } @@ -266,8 +275,7 @@ private void readElementCollection(DeployBeanPropertyAssocMany prop, ElementC elementDescriptor.addBeanProperty(valueProp); } - elementDescriptor.setName(prop.getFullBeanName()); - + elementDescriptor.setName(prop.toString()); factory.createUnidirectional(elementDescriptor, prop.getOwningType(), beanTable, prop.getTableJoin()); prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType(), scalar)); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java index 598962fcfd..6ca1243bb2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java @@ -122,7 +122,7 @@ private void readAssocOne(DeployBeanPropertyAssocOne prop) { } } - prop.setJoinType(prop.isNullable()); + prop.setJoinType(prop.isNullable() || prop.getForeignKey() != null && prop.getForeignKey().isNoConstraint()); if (!prop.getTableJoin().hasJoinColumns() && beanTable != null) { @@ -148,7 +148,7 @@ private void readAssocOne(DeployBeanPropertyAssocOne prop) { private void setFromJoinColumn(DeployBeanPropertyAssocOne prop, BeanTable beanTable, JoinColumn joinColumn) { if (beanTable == null) { - throw new IllegalStateException("Looks like a missing @ManyToOne or @OneToOne on property " + prop.getFullBeanName() + " - no related 'BeanTable'"); + throw new IllegalStateException("Looks like a missing @ManyToOne or @OneToOne on property " + prop + " - no related 'BeanTable'"); } prop.getTableJoin().addJoinColumn(util, false, joinColumn, beanTable); if (!joinColumn.updatable()) { @@ -210,15 +210,15 @@ private boolean readOrphanRemoval(OneToOne property) { private void readPrimaryKeyJoin(PrimaryKeyJoinColumn primaryKeyJoin, DeployBeanPropertyAssocOne prop) { if (!prop.isOneToOne()) { - throw new IllegalStateException("Expecting property " + prop.getFullBeanName() + " with PrimaryKeyJoinColumn to be a OneToOne?"); + throw new IllegalStateException("Expecting property " + prop + " with PrimaryKeyJoinColumn to be a OneToOne?"); } prop.setPrimaryKeyJoin(true); if (!primaryKeyJoin.name().isEmpty()) { - CoreLog.internal.log(INFO, "Automatically determining join columns for @PrimaryKeyJoinColumn - ignoring PrimaryKeyJoinColumn.name attribute [{0}] on {1}", primaryKeyJoin.name(), prop.getFullBeanName()); + CoreLog.internal.log(INFO, "Automatically determining join columns for @PrimaryKeyJoinColumn - ignoring PrimaryKeyJoinColumn.name attribute [{0}] on {1}", primaryKeyJoin.name(), prop); } if (!primaryKeyJoin.referencedColumnName().isEmpty()) { - CoreLog.internal.log(INFO, "Automatically determining join columns for @PrimaryKeyJoinColumn - Ignoring PrimaryKeyJoinColumn.referencedColumnName attribute [{0}] on {1}", primaryKeyJoin.referencedColumnName(), prop.getFullBeanName()); + CoreLog.internal.log(INFO, "Automatically determining join columns for @PrimaryKeyJoinColumn - Ignoring PrimaryKeyJoinColumn.referencedColumnName attribute [{0}] on {1}", primaryKeyJoin.referencedColumnName(), prop); } BeanTable baseBeanTable = factory.beanTable(info.getDescriptor().getBeanType()); String localPrimaryKey = baseBeanTable.getIdColumn(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationFields.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationFields.java index e4a3fca8c2..60ef0ddf7b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationFields.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationFields.java @@ -384,7 +384,7 @@ private boolean hasRelationshipItem(DeployBeanProperty prop) { } private void setEncryption(DeployBeanProperty prop, boolean dbEncString, int dbLen) { - util.checkEncryptKeyManagerDefined(prop.getFullBeanName()); + util.checkEncryptKeyManagerDefined(prop.toString()); ScalarType st = prop.getScalarType(); if (byte[].class.equals(st.type())) { // Always using Java client encryption rather than DB for encryption diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java index 31e6533f4c..d54a972be6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java @@ -2,6 +2,8 @@ import io.ebean.Model; import io.ebean.annotation.*; +import io.ebean.bean.ExtensionAccessor; +import io.ebean.bean.ExtensionAccessors; import io.ebean.core.type.ScalarType; import io.ebean.util.AnnotationUtil; import io.ebeaninternal.api.CoreLog; @@ -36,6 +38,15 @@ public DeployCreateProperties(TypeManager typeManager) { */ public void createProperties(DeployBeanDescriptor desc) { createProperties(desc, desc.getBeanType(), 0); + for (ExtensionAccessor info : ExtensionAccessors.read(desc.getBeanType())) { + createProperties(desc, info.getType(), 0); + for (DeployBeanProperty prop : desc.propertiesAll()) { + if (prop.getOwningType() == info.getType()) { + prop.setOwningType(desc.getBeanType()); + } + } + } + desc.sortProperties(); } @@ -83,7 +94,7 @@ private void createProperties(DeployBeanDescriptor desc, Class beanType, i DeployBeanProperty replaced = desc.addBeanProperty(prop); if (replaced != null && !replaced.isTransient()) { - String msg = "Huh??? property " + prop.getFullBeanName() + " being defined twice"; + String msg = "Huh??? property " + prop + " being defined twice"; msg += " but replaced property was not transient? This is not expected?"; CoreLog.log.log(WARNING, msg); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java index 027ecfc4cd..9fe23c0b1c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java @@ -106,7 +106,7 @@ void setEnumScalarType(Enumerated enumerated, DeployBeanProperty prop) { prop.setScalarType(scalarType); prop.setDbType(scalarType.jdbcType()); } catch (IllegalStateException e) { - throw new PersistenceException("Error mapping property " + prop.getFullBeanName() + " - " + e.getMessage()); + throw new PersistenceException("Error mapping property " + prop + " - " + e.getMessage()); } } @@ -141,7 +141,7 @@ private ScalarType scalarType(DeployBeanProperty property) { if (scalarType != null || property.isTransient()) { return scalarType; } - throw new PersistenceException(property.getFullBeanName() + " has no ScalarType - type " + propType.getName()); + throw new PersistenceException(property + " has no ScalarType - type " + propType.getName()); } catch (IllegalArgumentException e) { if (property.isTransient()) { // expected for transient properties with unknown/non-mapped types @@ -179,7 +179,7 @@ void setDbArray(DeployBeanProperty prop, DbArray dbArray) { Class type = prop.getPropertyType(); ScalarType scalarType = typeManager.dbArrayType(type, prop.getGenericType(), prop.isNullable()); if (scalarType == null) { - throw new RuntimeException("No ScalarType for @DbArray type for " + prop.getFullBeanName()); + throw new RuntimeException("No ScalarType for @DbArray type for " + prop); } int dbType = scalarType.jdbcType(); prop.setDbType(dbType); @@ -224,7 +224,7 @@ private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength, Mu /** * Return the JDBC type for the JSON storage type. */ - private int dbJsonStorage(DbJsonType dbJsonType) { + public static int dbJsonStorage(DbJsonType dbJsonType) { switch (dbJsonType) { case JSONB: return DbPlatformType.JSONB; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java index 67f4d99127..812196cae4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java @@ -1,5 +1,7 @@ package io.ebeaninternal.server.dto; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; import io.ebeaninternal.api.CoreLog; import io.ebeaninternal.server.type.TypeManager; @@ -7,7 +9,9 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import static java.lang.System.Logger.Level.DEBUG; @@ -22,10 +26,16 @@ final class DtoMetaBuilder { private final Class dtoType; private final List properties = new ArrayList<>(); private final List constructorList = new ArrayList<>(); + private final Set> annotationFilter = new HashSet<>(); DtoMetaBuilder(Class dtoType, TypeManager typeManager) { this.dtoType = dtoType; this.typeManager = typeManager; + annotationFilter.add(DbJson.class); + annotationFilter.add(DbJsonB.class); + if (typeManager.jsonMarkerAnnotation() != null) { + annotationFilter.add(typeManager.jsonMarkerAnnotation()); + } } DtoMeta build() { @@ -39,7 +49,7 @@ private void readProperties() { if (includeMethod(method)) { try { final String name = propertyName(method.getName()); - properties.add(new DtoMetaProperty(typeManager, dtoType, method, name)); + properties.add(new DtoMetaProperty(typeManager, dtoType, method, name, annotationFilter)); } catch (Exception e) { CoreLog.log.log(DEBUG, "exclude on " + dtoType + " method " + method, e); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java new file mode 100644 index 0000000000..d4519fe509 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java @@ -0,0 +1,81 @@ +package io.ebeaninternal.server.dto; + +import io.ebean.annotation.MutationDetection; +import io.ebeaninternal.server.deploy.meta.DeployProperty; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * DeployProperty for Dto-Properties. + * + * @author Roland Praml, FOCONIS AG + */ +class DtoMetaDeployProperty implements DeployProperty { + private final String name; + private final Class ownerType; + private final Type genericType; + private final Class propertyType; + private final Set metaAnnotations; + private final boolean nullable; + private MutationDetection mutationDetection = MutationDetection.DEFAULT; + + DtoMetaDeployProperty(String name, Class ownerType, Type genericType, Class propertyType, Set metaAnnotations, Method method) { + this.name = name; + this.ownerType = ownerType; + this.genericType = genericType; + this.nullable = !propertyType.isPrimitive(); + this.propertyType = propertyType; + this.metaAnnotations = metaAnnotations; + } + + @Override + public String getName() { + return name; + } + + @Override + public Type getGenericType() { + return genericType; + } + + @Override + public Class getPropertyType() { + return propertyType; + } + + @Override + public Class getOwnerType() { + return ownerType; + } + + @Override + public List getMetaAnnotations(Class annotationType) { + List result = new ArrayList<>(); + for (Annotation ann : metaAnnotations) { + if (ann.annotationType() == annotationType) { + result.add((A) ann); + } + } + return result; + } + + @Override + public MutationDetection getMutationDetection() { + return mutationDetection; + } + + @Override + public void setMutationDetection(MutationDetection mutationDetection) { + this.mutationDetection = mutationDetection; + } + + @Override + public boolean isNullable() { + return nullable; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java index 846c559ed0..06e3512721 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java @@ -1,15 +1,26 @@ package io.ebeaninternal.server.dto; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; +import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarType; +import io.ebean.util.AnnotationUtil; +import io.ebeaninternal.server.deploy.meta.DeployProperty; +import io.ebeaninternal.server.deploy.parse.DeployUtil; import io.ebeaninternal.server.type.TypeManager; +import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.sql.SQLException; +import java.util.Collections; +import java.util.List; +import java.util.Set; final class DtoMetaProperty implements DtoReadSet { @@ -20,18 +31,74 @@ final class DtoMetaProperty implements DtoReadSet { private final MethodHandle setter; private final ScalarType scalarType; - DtoMetaProperty(TypeManager typeManager, Class dtoType, Method writeMethod, String name) throws IllegalAccessException, NoSuchMethodException { + DtoMetaProperty(TypeManager typeManager, Class dtoType, Method writeMethod, String name, Set> annotationFilter) + throws IllegalAccessException, NoSuchMethodException { this.dtoType = dtoType; this.name = name; if (writeMethod != null) { this.setter = lookupMethodHandle(dtoType, writeMethod); - this.scalarType = typeManager.type(propertyType(writeMethod), propertyClass(writeMethod)); + Field field = findField(dtoType, name); + DeployProperty deployProp = new DtoMetaDeployProperty(name, + dtoType, + propertyType(writeMethod), + propertyClass(writeMethod), + field == null ? Collections.emptySet() : AnnotationUtil.metaFindAllFor(field, annotationFilter), + writeMethod); + scalarType = getScalarType(typeManager, deployProp); } else { this.scalarType = null; this.setter = null; } } + private ScalarType getScalarType(TypeManager typeManager, DeployProperty deployProp) { + final ScalarType scalarType; + + List json = deployProp.getMetaAnnotations(DbJson.class); + if (!json.isEmpty()) { + return typeManager.dbJsonType(deployProp, DeployUtil.dbJsonStorage(json.get(0).storage()), json.get(0).length()); + } + List jsonB = deployProp.getMetaAnnotations(DbJsonB.class); + if (!jsonB.isEmpty()) { + return typeManager.dbJsonType(deployProp, DbPlatformType.JSONB, jsonB.get(0).length()); + } + if (typeManager.jsonMarkerAnnotation() != null + && !deployProp.getMetaAnnotations(typeManager.jsonMarkerAnnotation()).isEmpty()) { + return typeManager.dbJsonType(deployProp, DbPlatformType.JSON, 0); + } + return typeManager.type(deployProp); + + + } + + /** + * Find all annotations on fields and methods. + */ + private Set findMetaAnnotations(Class dtoType, Method writeMethod, String name, Set> annotationFilter) { + Field field = findField(dtoType, name); + if (field != null) { + Set metaAnnotations = AnnotationUtil.metaFindAllFor(field, annotationFilter); + metaAnnotations.addAll(AnnotationUtil.metaFindAllFor(writeMethod, annotationFilter)); + return metaAnnotations; + } else { + return AnnotationUtil.metaFindAllFor(writeMethod, annotationFilter); + } + } + + /** + * Find field in class with same name + */ + private Field findField(Class type, String name) { + while (type != Object.class && type != null) { + try { + return dtoType.getDeclaredField(name); + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } + } + return null; + } + private static MethodHandle lookupMethodHandle(Class dtoType, Method method) throws NoSuchMethodException, IllegalAccessException { return LOOKUP.findVirtual(dtoType, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes())); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1IdGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1IdGenerator.java index 386f177c56..6d98a43e37 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1IdGenerator.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1IdGenerator.java @@ -130,7 +130,7 @@ private UuidV1IdGenerator(final File stateFile, String altNodeId) { saveState(); log.log(DEBUG, "Saved state: clockSeq {0}, timestamp {1}, uuid {2}, stateFile: {3})", clockSeq.get(), new Date(ts), uuid, stateFile); } catch (IOException e) { - log.log(ERROR, "There was a problem while detecting the nodeId. Falling back to random mode. Try using to specify 'ebean.uuidNodeId' property", e); + log.log(ERROR, "There was a problem while detecting the nodeId. Falling back to random mode. Try using to specify ''ebean.uuidNodeId'' property", e); useRandomMode(); } } @@ -147,7 +147,7 @@ private void tryHardwareId() throws IOException { try { nodeId = getHardwareId(); } catch (IOException e) { - log.log(ERROR, "Error while reading MAC address. Fall back to 'generate' mode", e); + log.log(ERROR, "Error while reading MAC address. Fall back to ''generate'' mode", e); tryGenerateMode(); } @@ -156,7 +156,7 @@ private void tryHardwareId() throws IOException { log.log(INFO, "Using MAC {0} to generate Type 1 UUIDs", getNodeIdentifier()); return; } - log.log(WARNING, "No suitable network interface found. Fall back to 'generate' mode"); + log.log(WARNING, "No suitable network interface found. Fall back to ''generate'' mode"); tryGenerateMode(); } @@ -212,7 +212,7 @@ private boolean restoreState() throws IOException { String propNodeId = prop.getProperty("nodeId"); if (propNodeId == null || propNodeId.isEmpty()) { - log.log(WARNING, "State file '{0}' is incomplete", stateFile); + log.log(WARNING, "State file ''{0}'' is incomplete", stateFile); return false; // we cannot restore } try { @@ -220,7 +220,7 @@ private boolean restoreState() throws IOException { nodeId = parseAlternativeNodeId(propNodeId); } else if (!getNodeIdentifier().equals(propNodeId)) { log.log(WARNING, - "The nodeId in the state file '{0}' has changed from {1} to {2}. " + "The nodeId in the state file ''{0}'' has changed from {1} to {2}. " + "This can happen when MAC address changes or when two containers share the same state file", stateFile, propNodeId, getNodeIdentifier()); return false; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java index f1b92a78bb..1f7fa08dcf 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java @@ -36,9 +36,9 @@ public PersistenceContext getPersistenceContext() { } @Override - public T read(T target) { + public T read() { try { - return desc.jsonRead(readJson, null, target); + return desc.jsonRead(readJson, null); } catch (IOException e) { throw new PersistenceIOException(e); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java index 340f978e1a..a711a1ea98 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java @@ -1,44 +1,28 @@ package io.ebeaninternal.server.json; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import io.ebean.FetchPath; import io.ebean.bean.EntityBean; import io.ebean.config.JsonConfig; import io.ebean.plugin.BeanType; -import io.ebean.text.json.EJson; -import io.ebean.text.json.JsonIOException; -import io.ebean.text.json.JsonReadOptions; -import io.ebean.text.json.JsonWriteBeanVisitor; -import io.ebean.text.json.JsonWriteOptions; +import io.ebean.plugin.Property; +import io.ebean.text.json.*; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiJsonContext; import io.ebeaninternal.api.json.SpiJsonReader; import io.ebeaninternal.api.json.SpiJsonWriter; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.type.TypeManager; import io.ebeaninternal.util.ParamTypeHelper; import io.ebeaninternal.util.ParamTypeHelper.ManyType; import io.ebeaninternal.util.ParamTypeHelper.TypeInfo; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; -import java.io.Writer; +import java.io.*; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; /** * Default implementation of JsonContext. @@ -124,7 +108,7 @@ public T toBean(Class cls, JsonParser parser) throws JsonIOException { public T toBean(Class cls, JsonParser parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = getDescriptor(cls); try { - return desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), false), null, null); + return desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options)), null); } catch (IOException e) { throw new JsonIOException(e); } @@ -136,8 +120,8 @@ public void toBean(T target, String json) throws JsonIOException { } @Override - public void toBean(T target, String json, JsonReadOptions options) throws JsonIOException { - toBean(target, new StringReader(json), options); + public void toBean(T target, String json, JsonReadOptions readOptions) throws JsonIOException { + toBean(target, new StringReader(json), readOptions); } @Override @@ -146,8 +130,8 @@ public void toBean(T target, Reader jsonReader) throws JsonIOException { } @Override - public void toBean(T target, Reader jsonReader, JsonReadOptions options) throws JsonIOException { - toBean(target, createParser(jsonReader), options); + public void toBean(T target, Reader jsonReader, JsonReadOptions readOptions) throws JsonIOException { + toBean(target, createParser(jsonReader), readOptions); } @Override @@ -157,10 +141,27 @@ public void toBean(T target, JsonParser parser) throws JsonIOException { @SuppressWarnings("unchecked") @Override - public void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException { + @Deprecated + public void toBean(T target, JsonParser parser, JsonReadOptions readOptions) throws JsonIOException { BeanDescriptor desc = (BeanDescriptor) getDescriptor(target.getClass()); try { - desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), target != null), null, target); + T bean = desc.jsonRead(new ReadJson(desc, parser, readOptions, determineObjectMapper(readOptions)), null); + desc.mergeBeans((EntityBean) bean, (EntityBean) target, null); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + + @Override + public T readProperty(Property property, JsonParser parser) { + return readProperty(property, parser, null); + } + + @Override + public T readProperty(Property property, JsonParser parser, JsonReadOptions options) { + BeanProperty prop = (BeanProperty) property; + try { + return (T) prop.jsonRead(new ReadJson(prop.descriptor(), parser, options, determineObjectMapper(options))); } catch (IOException e) { throw new JsonIOException(e); } @@ -169,13 +170,13 @@ public void toBean(T target, JsonParser parser, JsonReadOptions options) thr @Override public DJsonBeanReader createBeanReader(Class cls, JsonParser parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = getDescriptor(cls); - return new DJsonBeanReader<>(desc, new ReadJson(desc, parser, options, determineObjectMapper(options), false)); + return new DJsonBeanReader<>(desc, new ReadJson(desc, parser, options, determineObjectMapper(options))); } @Override public DJsonBeanReader createBeanReader(BeanType beanType, JsonParser parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = (BeanDescriptor) beanType; - SpiJsonReader readJson = new ReadJson(desc, parser, options, determineObjectMapper(options), false); + SpiJsonReader readJson = new ReadJson(desc, parser, options, determineObjectMapper(options)); return new DJsonBeanReader<>(desc, readJson); } @@ -207,7 +208,7 @@ public List toList(Class cls, JsonParser src) throws JsonIOException { @Override public List toList(Class cls, JsonParser src, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = getDescriptor(cls); - SpiJsonReader readJson = new ReadJson(desc, src, options, determineObjectMapper(options), false); + SpiJsonReader readJson = new ReadJson(desc, src, options, determineObjectMapper(options)); try { JsonToken currentToken = src.getCurrentToken(); @@ -221,7 +222,7 @@ public List toList(Class cls, JsonParser src, JsonReadOptions options) List list = new ArrayList<>(); do { // CHECKME: Should we update the list - T bean = desc.jsonRead(readJson, null, null); + T bean = desc.jsonRead(readJson, null); if (bean == null) { break; } else { @@ -353,6 +354,22 @@ private String toJsonString(Object value, JsonWriteOptions options, boolean pret return writer.toString(); } + @Override + public void writeProperty(Property property, Object bean, JsonGenerator generator) throws JsonIOException { + writeProperty(property, bean, generator, null); + } + + @Override + public void writeProperty(Property property, Object bean, JsonGenerator gen, JsonWriteOptions options) throws JsonIOException { + BeanProperty prop = (BeanProperty) property; + WriteJson writeJson = createWriteJson(gen, options); + try { + prop.jsonWrite(writeJson, (EntityBean) bean); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + @SuppressWarnings("unchecked") private void toJsonInternal(Object value, JsonGenerator gen, JsonWriteOptions options) throws IOException { if (value == null) { @@ -386,7 +403,7 @@ private void toJsonInternal(Object value, JsonGenerator gen, JsonWriteOptions op public SpiJsonReader createJsonRead(BeanType beanType, String json) { BeanDescriptor desc = (BeanDescriptor) beanType; JsonParser parser = createParser(new StringReader(json)); - return new ReadJson(desc, parser, null, defaultObjectMapper, false); + return new ReadJson(desc, parser, null, defaultObjectMapper); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java index ab9100c9f6..d5d637c8f6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.ObjectMapper; +import io.ebean.BeanMergeOptions; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; import io.ebean.bean.PersistenceContext; @@ -32,13 +33,12 @@ public final class ReadJson implements SpiJsonReader { private final Object objectMapper; private final PersistenceContext persistenceContext; private final LoadContext loadContext; - private final boolean update; private final boolean enableLazyLoading; /** * Construct with parser and readOptions. */ - public ReadJson(BeanDescriptor desc, JsonParser parser, JsonReadOptions readOptions, Object objectMapper, boolean update) { + public ReadJson(BeanDescriptor desc, JsonParser parser, JsonReadOptions readOptions, Object objectMapper) { this.rootDesc = desc; this.parser = parser; this.objectMapper = objectMapper; @@ -48,7 +48,6 @@ public ReadJson(BeanDescriptor desc, JsonParser parser, JsonReadOptions readO // only create visitorMap, pathStack if needed ... this.visitorMap = (readOptions == null) ? null : readOptions.getVisitorMap(); this.pathStack = (visitorMap == null && loadContext == null) ? null : new PathStack(); - this.update = update; } /** @@ -62,7 +61,6 @@ private ReadJson(JsonParser moreJson, ReadJson source) { this.objectMapper = source.objectMapper; this.persistenceContext = source.persistenceContext; this.loadContext = source.loadContext; - this.update = source.update; this.enableLazyLoading = source.enableLazyLoading; } @@ -119,9 +117,14 @@ public Object persistenceContextPutIfAbsent(Object id, EntityBean bean, BeanDesc // no persistenceContext means no lazy loading either return null; } - Object existing = beanDesc.contextPutIfAbsent(persistenceContext, id, bean); + EntityBean existing = beanDesc.contextPutIfAbsent(persistenceContext, id, bean); if (existing != null) { - beanDesc.merge(bean, (EntityBean) existing); + // we foind a bean in the persistence context AND we have deserialized the same bean + // so copy every property to the existing bean + BeanMergeOptions opts = new BeanMergeOptions(); + opts.setPersistenceContext(persistenceContext); + opts.setMergeVersion(true); + beanDesc.mergeBeans(bean, existing, opts); } else { if (loadContext != null) { EntityBeanIntercept ebi = bean._ebean_getIntercept(); @@ -192,7 +195,7 @@ public void popPath() { * call it's visit method with the bean and unmappedProperties. */ @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"unchecked", "rawtypes"}) public void beanVisitor(Object bean, Map unmappedProperties) { if (visitorMap != null) { JsonReadBeanVisitor visitor = visitorMap.get(pathStack.peekWithNull()); @@ -211,12 +214,4 @@ public void beanVisitor(Object bean, Map unmappedProperties) { public Object readValueUsingObjectMapper(Class propertyType) throws IOException { return mapper().readValue(parser, propertyType); } - - /** - * Do we update an existing bean? This meeans we have to set values via intercept and handle collections. - */ - @Override - public boolean update() { - return update; - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java index 39e03eb95e..f8a3574cad 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java @@ -506,6 +506,9 @@ private boolean isIncludeProperty(BeanProperty prop) { return true; if (currentIncludeProps != null) { // explicitly controlled by pathProperties + if (prop.isId() && currentIncludeProps.contains("${identifier}")) { + return true; + } return currentIncludeProps.contains(prop.name()); } else { // include only loaded properties diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java index 10cd8f5197..d663a3a6c0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java @@ -221,14 +221,21 @@ private void flushPstmtHolder(boolean reset) throws BatchedSqlException { * Execute all the requests contained in the list. */ void executeNow(ArrayList list) throws BatchedSqlException { - for (int i = 0; i < list.size(); i++) { - if (i % batchSize == 0) { - // hit the batch size so flush - flushPstmtHolder(); + boolean old = transaction.isFlushOnQuery(); + transaction.setFlushOnQuery(false); + // disable flush on query due transsaction callbacks + try { + for (int i = 0; i < list.size(); i++) { + if (i % batchSize == 0) { + // hit the batch size so flush + flushPstmtHolder(); + } + list.get(i).executeNow(); } - list.get(i).executeNow(); + flushPstmtHolder(); + } finally { + transaction.setFlushOnQuery(old); } - flushPstmtHolder(); } public void flushOnCommit() throws BatchedSqlException { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java index c63f326741..fe65a16894 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java @@ -7,11 +7,13 @@ import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.CoreLog; import io.ebeaninternal.api.SpiLogManager; -import io.ebeaninternal.server.core.timezone.DataTimeZone; import io.ebeaninternal.server.bind.DataBind; +import io.ebeaninternal.server.core.timezone.DataTimeZone; import io.ebeaninternal.server.expression.platform.DbExpressionHandler; import io.ebeaninternal.server.persist.platform.MultiValueBind; -import io.ebeaninternal.server.type.*; +import io.ebeaninternal.server.type.GeoTypeBinder; +import io.ebeaninternal.server.type.RsetDataReader; +import io.ebeaninternal.server.type.TypeManager; import javax.persistence.PersistenceException; import java.math.BigDecimal; @@ -34,15 +36,17 @@ public final class Binder { private final MultiValueBind multiValueBind; private final boolean enableBindLog; private final GeoTypeBinder geoTypeBinder; + private final int maxStringSize; public Binder(TypeManager typeManager, SpiLogManager logManager, int asOfBindCount, boolean asOfStandardsBased, - DbExpressionHandler dbExpressionHandler, DataTimeZone dataTimeZone, MultiValueBind multiValueBind) { + DbExpressionHandler dbExpressionHandler, DataTimeZone dataTimeZone, int maxStringSize, MultiValueBind multiValueBind) { this.typeManager = typeManager; this.geoTypeBinder = typeManager.geoTypeBinder(); this.asOfBindCount = asOfBindCount; this.asOfStandardsBased = asOfStandardsBased; this.dbExpressionHandler = dbExpressionHandler; this.dataTimeZone = dataTimeZone; + this.maxStringSize = maxStringSize; this.multiValueBind = multiValueBind; this.enableBindLog = logManager.enableBindLog(); } @@ -72,7 +76,7 @@ public boolean isAsOfStandardsBased() { * Bind the parameters to the preparedStatement returning the bind log. */ public String bind(BindParams bindParams, PreparedStatement statement, Connection connection) throws SQLException { - return bind(bindParams, new DataBind(dataTimeZone, statement, connection)); + return bind(bindParams, new DataBind(dataTimeZone, maxStringSize, statement, connection)); } /** @@ -394,7 +398,7 @@ public DbExpressionHandler getDbExpressionHandler() { * Create and return a DataBind for the statement. */ public DataBind dataBind(PreparedStatement stmt, Connection connection) { - return new DataBind(dataTimeZone, stmt, connection); + return new DataBind(dataTimeZone, maxStringSize, stmt, connection); } public DataReader createDataReader(ResultSet resultSet) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 24a5e4f1cb..79c11ba337 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -915,7 +915,7 @@ void deleteManyIntersection(EntityBean bean, BeanPropertyAssocMany many, SpiT private SpiSqlUpdate deleteAllIntersection(EntityBean bean, BeanPropertyAssocMany many, boolean publish) { IntersectionRow intRow = many.buildManyToManyDeleteChildren(bean, publish); - return intRow.createDeleteChildren(server); + return intRow.createDeleteChildren(server, many.extraWhere()); } /** @@ -939,7 +939,10 @@ private void deleteAssocMany(PersistRequestBean request) { for (BeanPropertyAssocOne prop : expOnes) { // for soft delete check cascade type also supports soft delete if (deleteMode.isHard() || prop.isTargetSoftDelete()) { - if (request.isLoadedProperty(prop)) { + if (prop.isPrimaryKeyExport()) { + // we can delete by id, neither if property loaded or not + delete(prop.targetDescriptor(), prop.descriptor().id(parentBean), null, t, deleteMode); + } else if (request.isLoadedProperty(prop)) { Object detailBean = prop.getValue(parentBean); if (detailBean != null) { deleteRecurse((EntityBean) detailBean, t, deleteMode); @@ -1009,7 +1012,7 @@ void deleteManyDetails(SpiTransaction t, BeanDescriptor desc, EntityBean pare if (targetDesc.isDeleteByStatement()) { // Just delete all the children with one statement IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds); - SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode); + SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode, many.extraWhere()); executeSqlUpdate(sqlDelete, t); } else { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocOne.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocOne.java index 939ace117a..23d5af54a1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocOne.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocOne.java @@ -55,6 +55,6 @@ private boolean isUpdate(Object beanId, Object outlineId, MergeRequest request) } private EntityBean getEntityBean(Object bean) { - return (EntityBean) one.value(bean); + return (EntityBean) one.getValue((EntityBean)bean); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java index ec2ab6a32f..dce34e9f9b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java @@ -91,6 +91,22 @@ private boolean isSaveIntersection() { // OneToMany JoinTable return true; } + if (many.isTableManaged()) { + List tables = new ArrayList<>(3); + tables.add(many.descriptor().baseTable()); + tables.add(many.targetDescriptor().baseTable()); + tables.add(many.intersectionTableJoin().getTable()); + // put all tables in a deterministic order + tables.sort(Comparator.naturalOrder()); + + if (transaction.isSaveAssocManyIntersection(String.join("-", tables), many.descriptor().rootName())) { + // notify others, that we do save this transaction + transaction.isSaveAssocManyIntersection(many.intersectionTableJoin().getTable(), many.descriptor().rootName()); + return true; + } else { + return false; + } + } return transaction.isSaveAssocManyIntersection(many.intersectionTableJoin().getTable(), many.descriptor().rootName()); } @@ -287,14 +303,16 @@ private void saveAssocManyIntersection(boolean queue) { } transaction.depth(+1); + boolean needsFlush = false; if (deletions != null && !deletions.isEmpty()) { for (Object other : deletions) { EntityBean otherDelete = (EntityBean) other; // the object from the 'other' side of the ManyToMany // build a intersection row for 'delete' IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherDelete, publish); - SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD); + SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD, many.extraWhere()); persister.executeOrQueue(sqlDelete, transaction, queue); + needsFlush = true; } } if (additions != null && !additions.isEmpty()) { @@ -309,7 +327,22 @@ private void saveAssocManyIntersection(boolean queue) { CoreLog.log.log(WARNING, msg); } else { if (!many.hasImportedId(otherBean)) { - throw new PersistenceException("ManyToMany bean does not have an Id value? " + otherBean); + throw new PersistenceException("ManyToMany bean " + otherBean + " does not have an Id value."); + } else if (many.getIntersectionFactory() != null) { + // build a intersection bean for 'insert' + // They need to be executed very late and would normally go to Queue#2, but we do not have + // a SpiSqlUpdate for now. + if (needsFlush) { + transaction.flushBatchOnCascade(); + } + if (queue) { + transaction.depth(+100); + } + Object intersectionBean = many.getIntersectionFactory().invoke(parentBean, otherBean); + persister.saveRecurse((EntityBean) intersectionBean, transaction, parentBean, request.flags()); + if (queue) { + transaction.depth(-100); + } } else { // build a intersection row for 'insert' IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherBean, publish); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java index 54d318a0ab..30c59fc547 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java @@ -2,12 +2,12 @@ import io.ebeaninternal.api.CoreLog; import io.ebeaninternal.api.SpiTransaction; +import io.ebeaninternal.server.bind.DataBind; import io.ebeaninternal.server.core.PersistRequestBean; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.persist.BatchedPstmt; import io.ebeaninternal.server.persist.BatchedPstmtHolder; import io.ebeaninternal.server.persist.dmlbind.BindableRequest; -import io.ebeaninternal.server.bind.DataBind; import javax.persistence.OptimisticLockException; import java.sql.Connection; @@ -66,7 +66,7 @@ public PersistRequestBean getPersistRequest() { * Bind to the statement returning the DataBind. */ DataBind bind(PreparedStatement stmt) { - return new DataBind(persistRequest.dataTimeZone(), stmt, transaction.getInternalConnection()); + return new DataBind(persistRequest.dataTimeZone(), persistRequest.maxStringSize(), stmt, transaction.getInternalConnection()); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/AbstractMetric.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/AbstractMetric.java new file mode 100644 index 0000000000..7c1400d50e --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/AbstractMetric.java @@ -0,0 +1,25 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.meta.MetricVisitor; +import io.ebean.metric.Metric; + +/** + * Used to collect counter metrics. + */ +abstract class AbstractMetric implements Metric { + + final String name; + private String reportName; + + AbstractMetric(String name) { + this.name = name; + } + + String reportName(MetricVisitor visitor) { + if (reportName == null) { + this.reportName = visitor.namingConvention().apply(name); + } + return reportName; + } + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetric.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetric.java index 34c47cfc64..d9c84bd067 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetric.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetric.java @@ -62,25 +62,4 @@ String reportName(MetricVisitor visitor) { return tmp; } - private static class DCountMetricStats implements CountMetricStats { - - private final String name; - private final long count; - - private DCountMetricStats(String name, long count) { - this.name = name; - this.count = count; - } - - @Override - public String name() { - return name; - } - - @Override - public long count() { - return count; - } - } - } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetricStats.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetricStats.java new file mode 100644 index 0000000000..1265c97ed9 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetricStats.java @@ -0,0 +1,27 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.metric.CountMetricStats; + +/** + * Holder for count metric values. + */ +class DCountMetricStats implements CountMetricStats { + + private final String name; + private final long count; + + DCountMetricStats(String name, long count) { + this.name = name; + this.count = count; + } + + @Override + public String name() { + return name; + } + + @Override + public long count() { + return count; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DIntMetric.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DIntMetric.java new file mode 100644 index 0000000000..f4e777ea4b --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DIntMetric.java @@ -0,0 +1,36 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.meta.MetricVisitor; +import io.ebean.metric.Metric; + +import java.util.function.IntSupplier; + +/** + * IntSupplier metric. + */ +final class DIntMetric implements Metric { + private final String name; + private final IntSupplier supplier; + private String reportName; + + DIntMetric(String name, IntSupplier supplier) { + this.name = name; + this.supplier = supplier; + } + + + @Override + public void visit(MetricVisitor visitor) { + int val = supplier.getAsInt(); + if (val != 0) { + final String name = reportName != null ? reportName : reportName(visitor); + visitor.visitCount(new DCountMetricStats(name, val)); + } + } + + String reportName(MetricVisitor visitor) { + final String tmp = visitor.namingConvention().apply(name); + this.reportName = tmp; + return tmp; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DLongMetric.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DLongMetric.java new file mode 100644 index 0000000000..ba291e4da4 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DLongMetric.java @@ -0,0 +1,36 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.meta.MetricVisitor; +import io.ebean.metric.Metric; + +import java.util.function.LongSupplier; + +/** + * LongSpplier metric. + */ +final class DLongMetric implements Metric { + private final String name; + private final LongSupplier supplier; + private String reportName; + + DLongMetric(String name, LongSupplier supplier) { + this.name = name; + this.supplier = supplier; + } + + + @Override + public void visit(MetricVisitor visitor) { + long val = supplier.getAsLong(); + if (val != 0) { + final String name = reportName != null ? reportName : reportName(visitor); + visitor.visitCount(new DCountMetricStats(name, val)); + } + } + + String reportName(MetricVisitor visitor) { + final String tmp = visitor.namingConvention().apply(name); + this.reportName = tmp; + return tmp; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java index d51ff2198d..5166524451 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java @@ -2,11 +2,15 @@ import io.ebean.ProfileLocation; import io.ebean.metric.CountMetric; +import io.ebean.metric.Metric; import io.ebean.metric.MetricFactory; import io.ebean.metric.QueryPlanMetric; import io.ebean.metric.TimedMetric; import io.ebean.metric.TimedMetricMap; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; + /** * Default metric factory implementation. */ @@ -27,6 +31,16 @@ public CountMetric createCountMetric(String name) { return new DCountMetric(name); } + @Override + public Metric createMetric(String name, LongSupplier supplier) { + return new DLongMetric(name, supplier); + } + + @Override + public Metric createMetric(String name, IntSupplier supplier) { + return new DIntMetric(name, supplier); + } + @Override public QueryPlanMetric createQueryPlanMetric(Class type, String label, ProfileLocation profileLocation, String sql) { return new DQueryPlanMetric(new DQueryPlanMeta(type, label, profileLocation, sql), new DTimedMetric(label)); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/properties/BeanPropertiesReader.java b/ebean-core/src/main/java/io/ebeaninternal/server/properties/BeanPropertiesReader.java index ec56da19de..251e113ba6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/properties/BeanPropertiesReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/properties/BeanPropertiesReader.java @@ -1,5 +1,8 @@ package io.ebeaninternal.server.properties; +import io.ebean.bean.ExtensionAccessor; +import io.ebean.bean.ExtensionAccessors; + import java.lang.reflect.Field; import java.util.Arrays; import java.util.HashMap; @@ -33,10 +36,22 @@ public Integer getPropertyIndex(String property) { return propertyIndexMap.get(property); } + private String[] concat(String[] arr1, String[] arr2) { + String[] ret = new String[arr1.length + arr2.length]; + System.arraycopy(arr1, 0, ret, 0, arr1.length); + System.arraycopy(arr2, 0, ret, arr1.length, arr2.length); + return ret; + } + private String[] getProperties(Class clazz) { try { Field field = clazz.getField("_ebean_props"); - return (String[]) field.get(null); + String[] props = (String[]) field.get(null); + + for (ExtensionAccessor extension : ExtensionAccessors.read(clazz)) { + props = concat(props, extension.getProperties()); + } + return props; } catch (Exception e) { throw new IllegalStateException("Error getting _ebean_props field on type " + clazz, e); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/properties/EnhanceBeanPropertyAccess.java b/ebean-core/src/main/java/io/ebeaninternal/server/properties/EnhanceBeanPropertyAccess.java index e7e48b6f03..ce42a69873 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/properties/EnhanceBeanPropertyAccess.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/properties/EnhanceBeanPropertyAccess.java @@ -54,12 +54,12 @@ private static final class Getter implements BeanPropertyGetter { @Override public Object get(EntityBean bean) { - return bean._ebean_getField(fieldIndex); + return bean._ebean_intercept().value(fieldIndex); } @Override public Object getIntercept(EntityBean bean) { - return bean._ebean_getFieldIntercept(fieldIndex); + return bean._ebean_intercept().valueIntercept(fieldIndex); } } @@ -73,12 +73,12 @@ private static final class Setter implements BeanPropertySetter { @Override public void set(EntityBean bean, Object value) { - bean._ebean_setField(fieldIndex, value); + bean._ebean_intercept().setValue(fieldIndex, value); } @Override public void setIntercept(EntityBean bean, Object value) { - bean._ebean_setFieldIntercept(fieldIndex, value); + bean._ebean_intercept().setValueIntercept(fieldIndex, value); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java index 283bc032d6..711e01bfde 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java @@ -385,7 +385,10 @@ public void setLazyLoadedChildBean(EntityBean bean, Object lazyLoadParentId) { this.lazyLoadParentId = lazyLoadParentId; } // add the loadedBean to the appropriate collection of lazyLoadParentBean - lazyLoadManyProperty.addBeanToCollectionWithCreate(lazyLoadParentBean, bean, true); + if (lazyLoadParentBean != null) { + // Note: parentBean can be null, when GC ran between construction of query and building the retrieved bean + lazyLoadManyProperty.addBeanToCollectionWithCreate(lazyLoadParentBean, bean, true); + } } } @@ -751,7 +754,4 @@ public void handleLoadError(String fullName, Exception e) { query.handleLoadError(fullName, e); } - public Set dependentTables() { - return queryPlan.dependentTables(); - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java index 4c72f053ee..b6e9bc98b3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java @@ -1,12 +1,17 @@ package io.ebeaninternal.server.query; -import io.ebeaninternal.api.SpiDbQueryPlan; -import io.ebeaninternal.api.SpiQueryBindCapture; -import io.ebeaninternal.api.SpiQueryPlan; +import io.ebean.config.CurrentTenantProvider; +import io.ebeaninternal.api.*; import io.ebeaninternal.server.bind.capture.BindCapture; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.Instant; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; +import static java.lang.System.Logger.Level.ERROR; + final class CQueryBindCapture implements SpiQueryBindCapture { private static final double multiplier = 1.5d; @@ -14,18 +19,21 @@ final class CQueryBindCapture implements SpiQueryBindCapture { private final ReentrantLock lock = new ReentrantLock(); private final CQueryPlanManager manager; private final SpiQueryPlan queryPlan; + private final CurrentTenantProvider tenantProvider; private BindCapture bindCapture; private long queryTimeMicros; private long thresholdMicros; private long captureCount; + private Object tenantId; private long lastBindCapture; - CQueryBindCapture(CQueryPlanManager manager, SpiQueryPlan queryPlan, long thresholdMicros) { + CQueryBindCapture(CQueryPlanManager manager, SpiQueryPlan queryPlan, long thresholdMicros, CurrentTenantProvider tenantProvider) { this.manager = manager; this.queryPlan = queryPlan; this.thresholdMicros = thresholdMicros; + this.tenantProvider = tenantProvider; } /** @@ -43,6 +51,7 @@ public void setBind(BindCapture bindCapture, long queryTimeMicros, long startNan this.thresholdMicros = Math.round(queryTimeMicros * multiplier); this.captureCount++; this.bindCapture = bindCapture; + this.tenantId = tenantProvider == null ? null : tenantProvider.currentId(); this.queryTimeMicros = queryTimeMicros; lastBindCapture = System.currentTimeMillis(); manager.notifyBindCapture(this, startNanos); @@ -53,28 +62,54 @@ public void setBind(BindCapture bindCapture, long queryTimeMicros, long startNan @Override public void queryPlanInit(long thresholdMicros) { - // effective enable bind capture for this plan - this.thresholdMicros = thresholdMicros; - this.captureCount = 0; + lock.lock(); + try { + // effective enable bind capture for this plan + this.thresholdMicros = thresholdMicros; + this.captureCount = 0; + } finally { + lock.unlock(); + } } /** * Collect the query plan using already captured bind values. */ - public boolean collectQueryPlan(CQueryPlanRequest request) { - if (bindCapture == null || request.since() < lastBindCapture) { - // no bind capture since the last capture - return false; - } + public boolean collectQueryPlan(CQueryPlanRequest request, SpiTransactionManager transactionManager) { - final BindCapture last = this.bindCapture; + final BindCapture last; + final Object tenant; + lock.lock(); + try { + if (bindCapture == null || request.since() < lastBindCapture) { + // no bind capture since the last capture + return false; + } + last = this.bindCapture; + tenant = this.tenantId; + } finally { + lock.unlock(); + } - SpiDbQueryPlan queryPlan = manager.collectPlan(request.connection(), this.queryPlan, last); - if (queryPlan != null) { - request.add(queryPlan.with(queryTimeMicros, captureCount)); - // effectively turn off bind capture for this plan - thresholdMicros = Long.MAX_VALUE; - return true; + final Instant whenCaptured = Instant.ofEpochMilli(this.lastBindCapture); + final long startNanos = System.nanoTime(); + try (Connection connection = transactionManager.queryPlanConnection(tenant)) { + SpiDbQueryPlan queryPlan = manager.collectPlan(connection, this.queryPlan, last); + if (queryPlan != null) { + final long captureMicros = TimeUnit.MICROSECONDS.convert(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + request.add(queryPlan.with(queryTimeMicros, captureCount, captureMicros, whenCaptured, tenant)); + // effectively turn off bind capture for this plan + lock.lock(); + try { + thresholdMicros = Long.MAX_VALUE; + } finally { + lock.unlock(); + } + return true; + } + } catch (SQLException e) { + CoreLog.log.log(ERROR, "Error during query plan collection", e); + return false; } return false; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java index 4078d511b7..4b1e2a1777 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java @@ -100,7 +100,6 @@ private > A findAttributeCollection(OrmQueryRequest r request.transaction().logSummary(rcQuery.summary()); } if (request.isQueryCachePut()) { - request.addDependentTables(rcQuery.dependentTables()); if (collection instanceof List) { collection = (A) Collections.unmodifiableList((List) collection); request.putToQueryCache(collection); @@ -167,7 +166,6 @@ public int findCount(OrmQueryRequest request) { request.transaction().end(); } if (request.isQueryCachePut()) { - request.addDependentTables(rcQuery.dependentTables()); request.putToQueryCache(count); } return count; @@ -355,9 +353,6 @@ BeanCollection findMany(OrmQueryRequest request) { cquery.auditFindMany(); } request.executeSecondaryQueries(false); - if (request.isQueryCachePut()) { - request.addDependentTables(cquery.dependentTables()); - } return beanCollection; } catch (SQLException e) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java index 78b20dc1c2..185be67b75 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java @@ -168,10 +168,6 @@ public void profile() { .addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.getProfileId()); } - Set dependentTables() { - return queryPlan.dependentTables(); - } - @Override public void cancel() { lock.lock(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java index ad52e4ecaa..0f8a5e4cbc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java @@ -8,11 +8,11 @@ import io.ebean.metric.MetricFactory; import io.ebean.metric.TimedMetric; import io.ebeaninternal.api.*; +import io.ebeaninternal.server.bind.DataBind; +import io.ebeaninternal.server.bind.DataBindCapture; import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.core.timezone.DataTimeZone; import io.ebeaninternal.server.query.CQueryPlanStats.Snapshot; -import io.ebeaninternal.server.bind.DataBind; -import io.ebeaninternal.server.bind.DataBindCapture; import io.ebeaninternal.server.type.RsetDataReader; import io.ebeaninternal.server.util.Md5; import io.ebeaninternal.server.util.Str; @@ -67,6 +67,7 @@ public class CQueryPlan implements SpiQueryPlan { private final CQueryPlanStats stats; private final Class beanType; final DataTimeZone dataTimeZone; + private final int maxStringSize; private final int asOfTableCount; /** @@ -82,6 +83,7 @@ public class CQueryPlan implements SpiQueryPlan { CQueryPlan(OrmQueryRequest request, SqlLimitResponse sqlRes, SqlTreePlan sqlTree, boolean rawSql, String logWhereSql) { this.server = request.server(); this.dataTimeZone = server.dataTimeZone(); + this.maxStringSize = server.maxStringSize(); this.beanType = request.descriptor().type(); this.planKey = request.queryPlanKey(); SpiQuery query = request.query(); @@ -107,6 +109,7 @@ public class CQueryPlan implements SpiQueryPlan { CQueryPlan(OrmQueryRequest request, String sql, SqlTreePlan sqlTree, String logWhereSql) { this.server = request.server(); this.dataTimeZone = server.dataTimeZone(); + this.maxStringSize = server.maxStringSize(); this.beanType = request.descriptor().type(); SpiQuery query = request.query(); this.profileLocation = query.getProfileLocation(); @@ -213,7 +216,7 @@ public DataReader createDataReader(ResultSet rset) { * Bind keys for encrypted properties if necessary returning the DataBind. */ final DataBind bindEncryptedProperties(PreparedStatement stmt, Connection conn) throws SQLException { - DataBind dataBind = new DataBind(dataTimeZone, stmt, conn); + DataBind dataBind = new DataBind(dataTimeZone, maxStringSize, stmt, conn); if (encryptedProps != null) { for (STreeProperty encryptedProp : encryptedProps) { dataBind.setString(encryptedProp.encryptKeyAsString()); @@ -223,7 +226,7 @@ final DataBind bindEncryptedProperties(PreparedStatement stmt, Connection conn) } private DataBindCapture bindCapture() throws SQLException { - DataBindCapture dataBind = DataBindCapture.of(dataTimeZone); + DataBindCapture dataBind = DataBindCapture.of(dataTimeZone, server.config().getMaxStringSize()); if (encryptedProps != null) { for (STreeProperty encryptedProp : encryptedProps) { dataBind.setString(encryptedProp.encryptKeyAsString()); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanManager.java index ac39f6beb6..4c75869b96 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanManager.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.query; +import io.ebean.config.CurrentTenantProvider; import io.ebean.meta.MetaQueryPlan; import io.ebean.meta.QueryPlanRequest; import io.ebean.metric.TimedMetric; @@ -8,11 +9,9 @@ import io.ebeaninternal.server.bind.capture.BindCapture; import java.sql.Connection; -import java.sql.SQLException; import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import static java.lang.System.Logger.Level.ERROR; import static java.util.Collections.emptyList; public final class CQueryPlanManager implements QueryPlanManager { @@ -21,13 +20,17 @@ public final class CQueryPlanManager implements QueryPlanManager { private final ConcurrentHashMap plans = new ConcurrentHashMap<>(); private final TransactionManager transactionManager; + private final CurrentTenantProvider tenantProvider; private final QueryPlanLogger planLogger; private final TimedMetric timeCollection; private final TimedMetric timeBindCapture; private long defaultThreshold; - public CQueryPlanManager(TransactionManager transactionManager, long defaultThreshold, QueryPlanLogger planLogger, ExtraMetrics extraMetrics) { + public CQueryPlanManager(TransactionManager transactionManager, + CurrentTenantProvider tenantProvider, + long defaultThreshold, QueryPlanLogger planLogger, ExtraMetrics extraMetrics) { this.transactionManager = transactionManager; + this.tenantProvider = tenantProvider; this.defaultThreshold = defaultThreshold; this.planLogger = planLogger; this.timeCollection = extraMetrics.getPlanCollect(); @@ -41,7 +44,7 @@ public void setDefaultThreshold(long thresholdMicros) { @Override public SpiQueryBindCapture createBindCapture(SpiQueryPlan queryPlan) { - return new CQueryBindCapture(this, queryPlan, defaultThreshold); + return new CQueryBindCapture(this, queryPlan, defaultThreshold, tenantProvider); } public void notifyBindCapture(CQueryBindCapture planBind, long startNanos) { @@ -58,16 +61,12 @@ public List collect(QueryPlanRequest request) { } private List collectPlans(QueryPlanRequest request) { - try (Connection connection = transactionManager.queryPlanConnection()) { - CQueryPlanRequest req = new CQueryPlanRequest(connection, request, plans.keySet().iterator()); + + CQueryPlanRequest req = new CQueryPlanRequest(transactionManager, request, plans.keySet().iterator()); while (req.hasNext()) { req.nextCapture(); } return req.plans(); - } catch (SQLException e) { - CoreLog.log.log(ERROR, "Error during query plan collection", e); - return emptyList(); - } } public SpiDbQueryPlan collectPlan(Connection connection, SpiQueryPlan queryPlan, BindCapture last) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRequest.java index 573b66026c..8441f4e4b1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRequest.java @@ -2,8 +2,8 @@ import io.ebean.meta.MetaQueryPlan; import io.ebean.meta.QueryPlanRequest; +import io.ebeaninternal.api.SpiTransactionManager; -import java.sql.Connection; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -15,14 +15,15 @@ final class CQueryPlanRequest { private final List plans = new ArrayList<>(); - private final Connection connection; + private final SpiTransactionManager transactionManager; private final long since; private final int maxCount; private final long maxTime; private final Iterator iterator; - CQueryPlanRequest(Connection connection, QueryPlanRequest req, Iterator iterator) { - this.connection = connection; + + CQueryPlanRequest(SpiTransactionManager transactionManager, QueryPlanRequest req, Iterator iterator) { + this.transactionManager = transactionManager; this.iterator = iterator; this.maxCount = req.maxCount(); long reqSince = req.since(); @@ -31,13 +32,6 @@ final class CQueryPlanRequest { this.maxTime = maxTimeMillis > 0 ? System.currentTimeMillis() + maxTimeMillis : 0; } - /** - * Return the connection used to collect the db query plan. - */ - Connection connection() { - return connection; - } - /** * Add the collected query plan. */ @@ -71,7 +65,7 @@ boolean hasNext() { */ void nextCapture() { final CQueryBindCapture next = iterator.next(); - if (next.collectQueryPlan(this)) { + if (next.collectQueryPlan(this, transactionManager)) { iterator.remove(); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java index 3b1865825c..513f645d2d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java @@ -140,10 +140,6 @@ public void profile() { .addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.getProfileId()); } - Set dependentTables() { - return queryPlan.dependentTables(); - } - @Override public void cancel() { lock.lock(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java index 642e163e2a..686e1420a9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java @@ -4,6 +4,8 @@ import io.ebean.meta.MetaQueryPlan; import io.ebeaninternal.api.SpiDbQueryPlan; +import java.time.Instant; + /** * Captured query plan details. */ @@ -19,6 +21,10 @@ final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan { private final String hash; private long queryTimeMicros; private long captureCount; + private long captureMicros; + private Instant whenCaptured; + + private Object tenantId; DQueryPlanOutput(Class beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) { this.beanType = beanType; @@ -80,6 +86,14 @@ public String plan() { return plan; } + /** + * Returns the tenant id of this plan. + */ + @Override + public Object tenantId() { + return tenantId; + } + /** * Return the query execution time associated with the capture of bind values used * to build the query plan. @@ -97,18 +111,39 @@ public long captureCount() { return captureCount; } + @Override + public long captureMicros() { + return captureMicros; + } + + @Override + public Instant whenCaptured() { + return whenCaptured; + } + @Override public String toString() { - return " BeanType:" + ((beanType == null) ? "" : beanType.getSimpleName()) + " planHash:" + hash + " label:" + label + " queryTimeMicros:" + queryTimeMicros + " captureCount:" + captureCount + "\n SQL:" + sql + "\nBIND:" + bind + "\nPLAN:" + plan; + return " BeanType:" + ((beanType == null) ? "" : beanType.getSimpleName()) + + " planHash:" + hash + + " label:" + label + + " queryTimeMicros:" + queryTimeMicros + + " captureCount:" + captureCount + + (tenantId == null ? "" : (" tenant:" + tenantId)) + + "\n SQL:" + sql + + "\nBIND:" + bind + + "\nPLAN:" + plan; } /** * Additionally set the query execution time and the number of bind captures. */ @Override - public DQueryPlanOutput with(long queryTimeMicros, long captureCount) { + public DQueryPlanOutput with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured, Object tenantId) { this.queryTimeMicros = queryTimeMicros; this.captureCount = captureCount; + this.captureMicros = captureMicros; + this.whenCaptured = whenCaptured; + this.tenantId = tenantId; return this; } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java index f4611617cd..b615fcd4ea 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java @@ -197,7 +197,7 @@ public String tableAlias(String prefix) { @Override public String tableAliasManyWhere(String prefix) { - return alias.tableAliasManyWhere(prefix); + return prefix == null ? tableAliasStack.peek() : alias.tableAliasManyWhere(prefix); } @Override @@ -225,9 +225,8 @@ public DefaultDbSqlContext append(String s) { } @Override - public void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType, String manyWhere) { + public void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType, String tableAlias) { // replace ${ta} placeholder with the real table alias... - String tableAlias = manyWhere == null ? tableAliasStack.peek() : tableAliasManyWhere(manyWhere); String converted = sqlFormulaJoin.replace(tableAliasPlaceHolder, tableAlias); if (formulaJoins == null) { formulaJoins = new HashSet<>(); @@ -240,11 +239,17 @@ public void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType, Strin formulaJoins.add(converted); sb.append(" "); if (joinType == SqlJoinType.OUTER) { - if ("join".equalsIgnoreCase(sqlFormulaJoin.substring(0, 4))) { + if ("join".equalsIgnoreCase(converted.substring(0, 4))) { // prepend left as we are in the 'many' part sb.append("left "); } } + if (joinType == SqlJoinType.INNER) { + if ("left join".equalsIgnoreCase(converted.substring(0, 9))) { + // remove left as we do not need it + converted = converted.substring(5); + } + } sb.append(converted); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java index a884a2d062..eed074283d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -160,8 +160,13 @@ public T findId(OrmQueryRequest request) { if (finder != null) { result = finder.postProcess(request, result); } - if (result != null && request.isBeanCachePut()) { - request.descriptor().cacheBeanPut((EntityBean) result); + if (result != null) { + if (request.isBeanCachePut()) { + request.descriptor().cacheBeanPut((EntityBean) result); + } + if (request.isQueryCachePut()) { + request.putToQueryCache(result); + } } return result; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java index 047b9f696b..7da7c1cd15 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java @@ -59,7 +59,7 @@ private String errMsg(String msg, String sql) { @Override public void findEach(RelationalQueryRequest request, RowConsumer consumer) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); request.mapEach(consumer); request.logSummary(); @@ -74,7 +74,7 @@ public void findEach(RelationalQueryRequest request, RowConsumer consumer) { @Override public void findEach(RelationalQueryRequest request, RowReader reader, Predicate consumer) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); while (request.next()) { if (!consumer.test(reader.read())) { break; @@ -93,7 +93,7 @@ public void findEach(RelationalQueryRequest request, RowReader reader, Pr @Override public T findOne(RelationalQueryRequest request, RowMapper mapper) { try { - request.executeSql(binder, SpiQuery.Type.BEAN); + request.executeSql(binder); T value = request.mapOne(mapper); request.logSummary(); return value; @@ -109,7 +109,7 @@ public T findOne(RelationalQueryRequest request, RowMapper mapper) { @Override public List findList(RelationalQueryRequest request, RowReader reader) { try { - request.executeSql(binder, SpiQuery.Type.LIST); + request.executeSql(binder); List rows = new ArrayList<>(); while (request.next()) { rows.add(reader.read()); @@ -129,7 +129,7 @@ public List findList(RelationalQueryRequest request, RowReader reader) public T findSingleAttribute(RelationalQueryRequest request, Class cls) { ScalarType scalarType = (ScalarType) binder.getScalarType(cls); try { - request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); + request.executeSql(binder); final DataReader dataReader = binder.createDataReader(request.getResultSet()); T value = null; if (dataReader.next()) { @@ -151,7 +151,7 @@ public T findSingleAttribute(RelationalQueryRequest request, Class cls) { public List findSingleAttributeList(RelationalQueryRequest request, Class cls) { ScalarType scalarType = (ScalarType) binder.getScalarType(cls); try { - request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); + request.executeSql(binder); final DataReader dataReader = binder.createDataReader(request.getResultSet()); List rows = new ArrayList<>(); while (dataReader.next()) { @@ -173,7 +173,7 @@ public List findSingleAttributeList(RelationalQueryRequest request, Class public void findSingleAttributeEach(RelationalQueryRequest request, Class cls, Consumer consumer) { ScalarType scalarType = (ScalarType) binder.getScalarType(cls); try { - request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); + request.executeSql(binder); final DataReader dataReader = binder.createDataReader(request.getResultSet()); while (dataReader.next()) { consumer.accept(scalarType.read(dataReader)); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java index 39588b7337..24ce404732 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java @@ -22,11 +22,12 @@ public DtoQueryEngine(Binder binder) { public List findList(DtoQueryRequest request) { try { - request.executeSql(binder, SpiQuery.Type.LIST); + request.executeSql(binder); List rows = new ArrayList<>(); while (request.next()) { rows.add(request.readNextBean()); } + request.putToQueryCache(rows); return rows; } catch (SQLException e) { @@ -38,7 +39,7 @@ public List findList(DtoQueryRequest request) { public QueryIterator findIterate(DtoQueryRequest request) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); return new DtoQueryIterator<>(request); } catch (SQLException e) { throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); @@ -47,7 +48,7 @@ public QueryIterator findIterate(DtoQueryRequest request) { public void findEach(DtoQueryRequest request, Consumer consumer) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); while (request.next()) { consumer.accept(request.readNextBean()); } @@ -61,7 +62,7 @@ public void findEach(DtoQueryRequest request, Consumer consumer) { public void findEach(DtoQueryRequest request, int batchSize, Consumer> consumer) { try { List buffer = new ArrayList<>(); - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); while (request.next()) { buffer.add(request.readNextBean()); if (buffer.size() >= batchSize) { @@ -82,7 +83,7 @@ public void findEach(DtoQueryRequest request, int batchSize, Consumer void findEachWhile(DtoQueryRequest request, Predicate consumer) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); while (request.next()) { if (!consumer.test(request.readNextBean())) { break; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java new file mode 100644 index 0000000000..568b9df5c5 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java @@ -0,0 +1,122 @@ + +package io.ebeaninternal.server.query; + +import io.ebean.util.IOUtils; +import io.ebean.util.StringHelper; +import io.ebeaninternal.api.CoreLog; +import io.ebeaninternal.api.SpiDbQueryPlan; +import io.ebeaninternal.api.SpiQueryPlan; +import io.ebeaninternal.server.bind.capture.BindCapture; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.Random; + +import static java.lang.System.Logger.Level.WARNING; + +/** + * A QueryPlanLogger for DB2. + *

+ * To use query plan capturing, you have to install the explain tables with + * SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA ). + * To do this in a repeatable script, you may use this statement: + * + *

+ * BEGIN
+ * IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN
+ *    call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA );
+ * END IF;
+ * END
+ * 
+ * + * @author Roland Praml, FOCONIS AG + */ +public final class QueryPlanLoggerDb2 extends QueryPlanLogger { + + private Random rnd = new Random(); + + private final String schema; + + private final boolean create; + + private static final String GET_PLAN_TEMPLATE = readReasource("QueryPlanLoggerDb2.sql"); + + private static final String CREATE_TEMPLATE = "BEGIN\n" + + "IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = ${SCHEMA} AND TABNAME = 'EXPLAIN_STREAM') THEN\n" + + " CALL SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', ${SCHEMA} );\n" + + "END IF;\n" + + "END"; + + public QueryPlanLoggerDb2(String opts) { + Map map = StringHelper.delimitedToMap(opts, ";", "="); + create = !"false" .equals(map.get("create")); // default is create + String schema = map.get("schema"); // should be null or SYSTOOLS + if (schema == null || schema.isEmpty()) { + this.schema = null; + } else { + this.schema = schema.toUpperCase(); + } + } + + private static String readReasource(String resName) { + try (InputStream stream = QueryPlanLoggerDb2.class.getResourceAsStream(resName)) { + if (stream == null) { + throw new IllegalStateException("Could not find resource " + resName); + } + BufferedReader reader = IOUtils.newReader(stream); + StringBuilder sb = new StringBuilder(); + reader.lines().forEach(line -> sb.append(line).append('\n')); + return sb.toString(); + } catch (IOException e) { + throw new IllegalStateException("Could not read resource " + resName, e); + } + } + + @Override + public SpiDbQueryPlan collectPlan(Connection conn, SpiQueryPlan plan, BindCapture bind) { + try (Statement stmt = conn.createStatement()) { + if (create) { + // create explain tables if neccessary + if (schema == null) { + stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "CURRENT USER")); + } else { + stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "'" + schema + "'")); + } + conn.commit(); + } + + try { + int queryNo = rnd.nextInt(Integer.MAX_VALUE); + + String sql = "EXPLAIN PLAN SET QUERYNO = " + queryNo + " FOR " + plan.sql(); + try (PreparedStatement explainStmt = conn.prepareStatement(sql)) { + bind.prepare(explainStmt, conn); + explainStmt.execute(); + } + + sql = schema == null + ? GET_PLAN_TEMPLATE.replace("${SCHEMA}", conn.getMetaData().getUserName().toUpperCase()) + : GET_PLAN_TEMPLATE.replace("${SCHEMA}", schema); + + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setInt(1, queryNo); + try (ResultSet rset = pstmt.executeQuery()) { + return readQueryPlan(plan, bind, rset); + } + } + } finally { + conn.rollback(); // do not keep query plans in DB + } + } catch (SQLException e) { + CoreLog.log.log(WARNING, "Could not log query plan", e); + return null; + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java index 449b659261..06e5de5b58 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java @@ -87,7 +87,7 @@ public interface STreeType { /** * Put the entity bean into the persistence context. */ - Object contextPutIfAbsent(PersistenceContext persistenceContext, Object id, EntityBean localBean); + EntityBean contextPutIfAbsent(PersistenceContext persistenceContext, Object id, EntityBean localBean); /** * Set draft status on the entity bean. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java index 3c0a09dcb4..db0d0bef6f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java @@ -673,11 +673,6 @@ private SqlTreeNodeExtraJoin findExtraJoinRoot(String includeProp, SqlTreeNodeEx } else { // look in register ... String parentPropertyName = includeProp.substring(0, dotPos); - if (selectIncludes.contains(parentPropertyName)) { - // parent already handled by select - return childJoin; - } - SqlTreeNodeExtraJoin parentJoin = joinRegister.get(parentPropertyName); if (parentJoin == null) { // we need to create this the parent implicitly... diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java index bd6f62d57f..d6aac1f6d0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java @@ -183,7 +183,7 @@ private void readId() throws SQLException { private void readIdBean() { // check the PersistenceContext to see if the bean already exists - contextBean = (EntityBean) localDesc.contextPutIfAbsent(persistenceContext, id, localBean); + contextBean = localDesc.contextPutIfAbsent(persistenceContext, id, localBean); if (contextBean == null) { // bean just added to the persistenceContext contextBean = localBean; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 45866375d6..fbf6f006cd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -163,6 +163,7 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { private String nativeSql; private boolean orderById; private ProfileLocation profileLocation; + private Class dtoType = Object.class; // default: Object saves some null-checks. public DefaultOrmQuery(BeanDescriptor desc, SpiEbeanServer server, ExpressionFactory expressionFactory) { this.beanDescriptor = desc; @@ -1186,7 +1187,7 @@ public final HashQuery queryHash() { // so queryPlanHash is calculated well before this method is called BindValuesKey bindKey = new BindValuesKey(); queryBindKey(bindKey); - return new HashQuery(queryPlanKey, bindKey); + return new HashQuery(queryPlanKey, bindKey, dtoType); } @Override @@ -1649,6 +1650,11 @@ public final void setManualId() { } } + @Override + public void setDtoType(Class dtoType) { + this.dtoType = dtoType; + } + /** * return true if user specified to use SQL DISTINCT (effectively excludes id property). */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java index f665662f6c..2c0b24f0b8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java @@ -263,6 +263,15 @@ public void setReadOnly(boolean readOnly) { public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) { } + @Override + public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) { + } + + @Override + public boolean isOverwriteGeneratedProperties() { + return true; + } + @Override public Boolean isUpdateAllLoadedProperties() { return null; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java index a432a54d46..67146d70b1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java @@ -53,6 +53,7 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes { private boolean queryOnly = true; private boolean localReadOnly; private Boolean updateAllLoadedProperties; + private boolean overwriteGeneratedProperties = true; private boolean oldBatchMode; private boolean batchMode; private boolean batchOnCascadeMode; @@ -190,7 +191,6 @@ final void checkAutoCommit(Connection connection) throws SQLException { } - @Override public final void setAutoPersistUpdates(boolean autoPersistUpdates) { this.autoPersistUpdates = autoPersistUpdates; @@ -251,7 +251,7 @@ public final void register(TransactionCallback callback) { callbackList.add(callback); } - private void withEachCallback(Consumer consumer) { + private void withEachCallbackFailSilent(Consumer consumer) { if (callbackList != null) { // using old style loop to cater for case when new callbacks are added recursively (as otherwise iterator fails fast) for (int i = 0; i < callbackList.size(); i++) { @@ -259,17 +259,27 @@ private void withEachCallback(Consumer consumer) { consumer.accept(callbackList.get(i)); } catch (Exception e) { log.log(ERROR, "Error executing transaction callback", e); + throw wrapIfNeeded(e); } } } } + private void withEachCallback(Consumer consumer) { + if (callbackList != null) { + // using old style loop to cater for case when new callbacks are added recursively (as otherwise iterator fails fast) + for (int i = 0; i < callbackList.size(); i++) { + consumer.accept(callbackList.get(i)); + } + } + } + private void firePreRollback() { - withEachCallback(TransactionCallback::preRollback); + withEachCallbackFailSilent(TransactionCallback::preRollback); } private void firePostRollback() { - withEachCallback(TransactionCallback::postRollback); + withEachCallbackFailSilent(TransactionCallback::postRollback); if (changeLogHolder != null) { changeLogHolder.postRollback(); } @@ -439,6 +449,16 @@ public final Boolean isUpdateAllLoadedProperties() { return updateAllLoadedProperties; } + @Override + public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) { + this.overwriteGeneratedProperties = overwriteGeneratedProperties; + } + + @Override + public boolean isOverwriteGeneratedProperties() { + return overwriteGeneratedProperties; + } + @Override public final void setBatchMode(boolean batchMode) { this.batchMode = batchMode; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java index b0ee596df5..a4fa0f65d5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java @@ -226,6 +226,15 @@ public void setPersistCascade(boolean persistCascade) { public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) { } + @Override + public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) { + } + + @Override + public boolean isOverwriteGeneratedProperties() { + return true; + } + @Override public void setSkipCache(boolean skipCache) { } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java index 442a25ff89..d53c41b99f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java @@ -247,8 +247,8 @@ public final String name() { } @Override - public final Connection queryPlanConnection() throws SQLException { - return dataSourceSupplier.getConnection(null); + public final Connection queryPlanConnection(Object tenantId) throws SQLException { + return dataSourceSupplier.getConnection(tenantId); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index 9177eed465..ca5312427c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -18,10 +18,12 @@ import io.ebeaninternal.server.core.ServiceUtil; import io.ebeaninternal.server.core.bootup.BootupClasses; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; +import io.ebeaninternal.server.deploy.meta.DeployProperty; import javax.persistence.AttributeConverter; import javax.persistence.EnumType; import java.io.File; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; @@ -56,7 +58,7 @@ public final class DefaultTypeManager implements TypeManager { private final DefaultTypeFactory extraTypeFactory; - private final ScalarType fileType = new ScalarTypeFile(); + private final ScalarType fileType; private final ScalarType hstoreType = new ScalarTypePostgresHstore(); private final JsonConfig.DateTime jsonDateTime; @@ -94,9 +96,9 @@ public DefaultTypeManager(DatabaseConfig config, BootupClasses bootupClasses) { this.arrayTypeSetFactory = arrayTypeSetFactory(config.getDatabasePlatform()); this.offlineMigrationGeneration = DbOffline.isGenerateMigration(); this.defaultEnumType = config.getDefaultEnumType(); + this.fileType = new ScalarTypeFile(config.getTempFileProvider()); - ServiceLoader mappers = ServiceLoader.load(ScalarJsonMapper.class); - jsonMapper = mappers.findFirst().orElse(null); + jsonMapper = findJsonMapper(); initialiseStandard(config); initialiseJavaTimeTypes(config); @@ -110,6 +112,26 @@ public DefaultTypeManager(DatabaseConfig config, BootupClasses bootupClasses) { } } + /** + * Searches the JsonMapper and checks if markerAnnotation is on class path. + */ + private static ScalarJsonMapper findJsonMapper() { + ServiceLoader mappers = ServiceLoader.load(ScalarJsonMapper.class); + ScalarJsonMapper mapper = mappers.findFirst().orElse(null); + if (mapper != null) { + try { + if (mapper.markerAnnotation() != null) { + return mapper; + } else { + log.log(System.Logger.Level.WARNING, "Not using {0}, because markerAnnotation is null", mapper.getClass().getName()); + } + } catch (NoClassDefFoundError e) { + log.log(System.Logger.Level.WARNING, "Not using {0}, because markerAnnotation is inacessible ({1})", mapper.getClass().getName(), e.getMessage()); + } + } + return null; + } + private void loadGeoTypeBinder(DatabaseConfig config) { GeoTypeProvider provider = config.getServiceObject(GeoTypeProvider.class); if (provider == null) { @@ -191,7 +213,8 @@ public ScalarType type(int jdbcType) { } @Override - public ScalarType type(Type propertyType, Class propertyClass) { + public ScalarType type(DeployProperty prop) { + Type propertyType = prop.getGenericType(); if (propertyType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) propertyType; Type rawType = pt.getRawType(); @@ -199,7 +222,7 @@ public ScalarType type(Type propertyType, Class propertyClass) { return dbArrayType((Class) rawType, propertyType, true); } } - return type(propertyClass); + return type(prop.getPropertyType()); } /** @@ -297,8 +320,14 @@ private boolean isEnumType(Type valueType) { return TypeReflectHelper.isEnumType(valueType); } + + @Override + public Class jsonMarkerAnnotation() { + return jsonMapper == null ? null : jsonMapper.markerAnnotation(); + } + @Override - public ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLength) { + public ScalarType dbJsonType(DeployProperty prop, int dbType, int dbLength) { Class type = prop.getPropertyType(); if (type.equals(String.class)) { return ScalarTypeJsonString.typeFor(postgres, dbType); @@ -328,14 +357,14 @@ public ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLengt return createJsonObjectMapperType(prop, dbType, DocPropertyType.OBJECT); } - private boolean keepSource(DeployBeanProperty prop) { + private boolean keepSource(DeployProperty prop) { if (prop.getMutationDetection() == MutationDetection.DEFAULT) { prop.setMutationDetection(jsonManager.mutationDetection()); } return prop.getMutationDetection() == MutationDetection.SOURCE; } - private DocPropertyType docPropertyType(DeployBeanProperty prop, Class type) { + private DocPropertyType docPropertyType(DeployProperty prop, Class type) { return type.equals(List.class) || type.equals(Set.class) ? docType(prop.getGenericType()) : DocPropertyType.OBJECT; } @@ -369,14 +398,18 @@ private boolean isMapValueTypeObject(Type genericType) { return Object.class.equals(typeArgs[1]) || "?".equals(typeArgs[1].toString()); } - private ScalarType createJsonObjectMapperType(DeployBeanProperty prop, int dbType, DocPropertyType docType) { + private ScalarType createJsonObjectMapperType(DeployProperty prop, int dbType, DocPropertyType docType) { if (jsonMapper == null) { throw new IllegalArgumentException("Unsupported @DbJson mapping - Jackson ObjectMapper not present for " + prop); } if (MutationDetection.DEFAULT == prop.getMutationDetection()) { prop.setMutationDetection(jsonManager.mutationDetection()); } - var req = new ScalarJsonRequest(jsonManager, dbType, docType, prop.getDesc().getBeanType(), prop.getMutationDetection(), prop.getName()); + Class type = prop.getOwnerType(); + if (prop instanceof DeployBeanProperty) { + type = ((DeployBeanProperty) prop).getField().getDeclaringClass(); + } + var req = new ScalarJsonRequest(jsonManager, dbType, docType, type, prop.getMutationDetection(), prop.getName()); return jsonMapper.createType(req); } @@ -511,7 +544,7 @@ public ScalarType enumType(Class> enumType, EnumType type) ScalarTypeEnum scalarEnum = (ScalarTypeEnum) scalarType; if (scalarEnum != null && !scalarEnum.isOverrideBy(type)) { if (type != null && !scalarEnum.isCompatible(type)) { - throw new IllegalStateException("Error mapping Enum type:" + enumType + " It is mapped using 2 different modes when only one is supported (ORDINAL, STRING or an Ebean mapping)"); + throw new IllegalStateException("Error mapping Enum type:" + enumType + " It is mapped using 2 of (ORDINAL, STRING or an Ebean mapping) when only one is supported."); } return scalarEnum; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBoolean.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBoolean.java index 62aa6c5538..6719200024 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBoolean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBoolean.java @@ -2,12 +2,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import io.ebean.core.type.DataBinder; -import io.ebean.core.type.DataReader; -import io.ebean.core.type.DocPropertyType; -import io.ebean.core.type.ScalarTypeBase; -import io.ebean.core.type.BasicTypeConverter; +import io.ebean.core.type.*; import java.io.DataInput; import java.io.DataOutput; @@ -348,8 +343,8 @@ public void writeData(DataOutput dataOutput, Boolean val) throws IOException { } @Override - public Boolean jsonRead(JsonParser parser) { - return JsonToken.VALUE_TRUE == parser.getCurrentToken() ? Boolean.TRUE : Boolean.FALSE; + public Boolean jsonRead(JsonParser parser) throws IOException { + return parser.getValueAsBoolean(); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDouble.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDouble.java index 3fe5deba07..0874bfe825 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDouble.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDouble.java @@ -78,7 +78,7 @@ public void writeData(DataOutput dataOutput, Double value) throws IOException { @Override public Double jsonRead(JsonParser parser) throws IOException { - return parser.getDoubleValue(); + return parser.getValueAsDouble(); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java index 5e4ad5df80..5dd6ff96e9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import io.ebean.config.TempFileProvider; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -20,26 +21,22 @@ */ final class ScalarTypeFile extends ScalarTypeBase { - private final String prefix; - private final String suffix; - private final File directory; + private final TempFileProvider tempFileProvider; private final int bufferSize; /** - * Construct with reasonable defaults of Blob and 8096 buffer size. + * Construct with reasonable defaults of Blob and 8192 buffer size. */ - ScalarTypeFile() { - this(Types.LONGVARBINARY, "db-", null, null, 8096); + ScalarTypeFile(TempFileProvider tempFileProvider) { + this(Types.LONGVARBINARY, tempFileProvider, 8192); } /** * Create the ScalarTypeFile. */ - ScalarTypeFile(int jdbcType, String prefix, String suffix, File directory, int bufferSize) { + ScalarTypeFile(int jdbcType, TempFileProvider tempFileProvider, int bufferSize) { super(File.class, false, jdbcType); - this.prefix = prefix; - this.suffix = suffix; - this.directory = directory; + this.tempFileProvider = tempFileProvider; this.bufferSize = bufferSize; } @@ -66,7 +63,7 @@ public File read(DataReader reader) throws SQLException { } try { // stream from db into our temp file - File tempFile = File.createTempFile(prefix, suffix, directory); + File tempFile = tempFileProvider.createTempFile(); OutputStream os = getOutputStream(tempFile); pump(is, os); return tempFile; @@ -109,7 +106,7 @@ public void jsonWrite(JsonGenerator writer, File value) throws IOException { @Override public File jsonRead(JsonParser parser) throws IOException { - File tempFile = File.createTempFile(prefix, suffix, directory); + File tempFile = tempFileProvider.createTempFile(); try (OutputStream os = getOutputStream(tempFile)) { parser.readBinaryValue(os); os.flush(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java index aea3b8d166..c4a2a7b1f3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -88,7 +89,7 @@ public Integer parse(String value) { @Override public Integer jsonRead(JsonParser parser) throws IOException { - return parser.getIntValue(); + return parser.getValueAsInt(); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLong.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLong.java index b04a0a9369..6ee80ccea2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLong.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLong.java @@ -88,7 +88,7 @@ public void writeData(DataOutput dataOutput, Long value) throws IOException { @Override public Long jsonRead(JsonParser parser) throws IOException { - return parser.getLongValue(); + return parser.getValueAsLong(); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeYear.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeYear.java index 9c60047f54..071d20e1ea 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeYear.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeYear.java @@ -83,7 +83,7 @@ public Year parse(String value) { @Override public Year jsonRead(JsonParser parser) throws IOException { - return Year.of(parser.getIntValue()); + return Year.of(parser.getValueAsInt()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java index e683b43e61..828174a677 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java @@ -2,8 +2,10 @@ import io.ebean.core.type.ScalarType; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; +import io.ebeaninternal.server.deploy.meta.DeployProperty; import javax.persistence.EnumType; +import java.lang.annotation.Annotation; import java.lang.reflect.Type; /** @@ -40,20 +42,25 @@ public interface TypeManager { *

* For example Array based ScalarType for types like {@code List}. */ - ScalarType type(Type propertyType, Class type); + ScalarType type(DeployProperty property); /** * Create a ScalarType for an Enum using a mapping (rather than JPA Ordinal or String which has limitations). */ ScalarType enumType(Class> enumType, EnumType enumerated); + /** + * Returns the Json Marker annotation (e.g. JacksonAnnotation) + */ + Class jsonMarkerAnnotation(); + /** * Return the ScalarType used to handle JSON content. *

* Note that type expected to be JsonNode or Map. *

*/ - ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLength); + ScalarType dbJsonType(DeployProperty prop, int dbType, int dbLength); /** * Return the ScalarType used to handle DB ARRAY. diff --git a/ebean-core/src/main/resources/io/ebeaninternal/server/core/metrics.css b/ebean-core/src/main/resources/io/ebeaninternal/server/core/metrics.css new file mode 100644 index 0000000000..b2a82ed96a --- /dev/null +++ b/ebean-core/src/main/resources/io/ebeaninternal/server/core/metrics.css @@ -0,0 +1,113 @@ +@charset "UTF-8"; +.sortable th { + cursor: pointer; +} + +.sortable th::after, .sortable th::before { + transition: color 0.1s ease-in-out; + font-size: 1.2em; + color: transparent; +} +.sortable tr.sortHdr th::after { + margin-left: 3px; + content: "▸"; +} +.sortable tr.sortHdr th:hover::after { + color: inherit; +} +.sortable tr.sortHdr th.dir-d::after { + color: inherit; + content: "▾"; +} +.sortable tr.sortHdr th.dir-u::after { + color: inherit; + content: "▴"; +} + +.sortable .number { + text-align: right; + white-space: nowrap; +} +.sortable .hash { + white-space: nowrap; +} +/* raw number display */ +.sortable .number div:nth-child(2) { + display: none; +} +#raw:checked ~ div .sortable .number div:nth-child(1) { + display: none; +} +#raw:checked ~ div .sortable .number div:nth-child(2) { + display: block; +} + +.sortable .filtered { + display: none; +} +.sortable { + --stripe-color: #e4e4e4; + --th-color: #fff; + --th-bg: #808080; + --td-color: #000; + --td-on-stripe-color: #000; + border-spacing: 0; + min-width: 640px; +} +.sortable tbody tr:nth-child(odd) { + background-color: var(--stripe-color); + color: var(--td-on-stripe-color); +} +.sortable th { + background: var(--th-bg); + color: var(--th-color); + font-weight: normal; + text-align: left; + text-transform: capitalize; + vertical-align: baseline; + white-space: nowrap; +} + +.sortable td { + color: var(--td-color); +} +.sortable td, +.sortable th { + padding: 5px; + overflow-wrap: anywhere; +} + +body { + font-size: 14px; +} + +p { + line-height: 1.7em; +} + +code { + font-family: monospace; + background: #eee; + padding: 5px; + border-radius: 2px; +} + +* { + box-sizing: border-box; + font-family: -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.plan { display : none; } + +#plans:checked ~ div table tbody tr td code { + display: block; + white-space: pre-wrap; +} + +.bind { display : none; } +#binds:checked ~ div table tbody tr td i { + display: block; + white-space: pre-wrap; +} diff --git a/ebean-core/src/main/resources/io/ebeaninternal/server/core/metrics.js b/ebean-core/src/main/resources/io/ebeaninternal/server/core/metrics.js new file mode 100644 index 0000000000..ef299ea51d --- /dev/null +++ b/ebean-core/src/main/resources/io/ebeaninternal/server/core/metrics.js @@ -0,0 +1,216 @@ +/** + * The sort code is adapted from https://github.com/tofsjonas/sortable + * Copyleft 2017 Jonas Earendel + * License: http://unlicense.org + */ + + (function() { // start closure +function findElementRecursive(element, tag) { + return !element ? null : element.nodeName === tag ? element : findElementRecursive(element.parentNode, tag) +} + +function getValue(element) { + return element.getAttribute('data-sort') || element.innerText +} + +document.addEventListener('click', function (e) { + try { + var descending_th_class = ' dir-d ' + var ascending_th_class = ' dir-u ' + var ascending_table_sort_class = 'asc' + var regex_dir = / dir-(u|d) / + var element = findElementRecursive(e.target, 'TH') + var tr = findElementRecursive(element, 'TR') + var table = findElementRecursive(tr, 'TABLE') + + function reClassify(element, dir) { + element.className = element.className.replace(regex_dir, '') + dir + } + + if (table && tr && tr.className == 'sortHdr') { + var column_index + var nodes = tr.cells + // Reset thead cells and get column index + for (var i = 0; i < nodes.length; i++) { + if (nodes[i] === element) { + column_index = i + } else { + reClassify(nodes[i], '') + } + } + var dir = descending_th_class + + // Check if we're sorting ascending or descending + if (element.className.indexOf(descending_th_class) !== -1 + || (table.className.indexOf(ascending_table_sort_class) !== -1 + && element.className.indexOf(ascending_th_class) == -1)) { + dir = ascending_th_class + } + + // Update the `th` class accordingly + reClassify(element, dir) + + // Get the array rows in an array, so we can sort them... + var rows = [].slice.call(table.tBodies[0].rows, 0) + var reverse = dir === ascending_th_class + + // Sort them using Array.prototype.sort() + rows.sort(function (a, b) { + var x = getValue((reverse ? a : b).cells[column_index]) + var y = getValue((reverse ? b : a).cells[column_index]) + return x.length && y.length && !isNaN(x - y) ? x - y : x.localeCompare(y) + }) + + // Make a clone without content + var clone_tbody = table.tBodies[0].cloneNode() + // Fill it with the sorted values + while (rows.length) { + clone_tbody.appendChild(rows.splice(0, 1)[0]) + } + // And finally replace the unsorted table with the sorted one + table.replaceChild(clone_tbody, table.tBodies[0]) + } + } catch (error) { + console.log(error) + } +}) + +function getIndex(collection, element) { + for (var i = 0; i < collection.length; i++) { + if (collection[i] === element) return i; + } + return -1; +} + +function updateFilterValues() { + for (var i = 0; i < filters.length; i++) { + var filter = filters[i]; + var table = findElementRecursive(filter, 'TABLE') + var cell = findElementRecursive(filter, 'TH') + var tr = findElementRecursive(filter, 'TR') + var idx = getIndex(tr.cells, cell) + var option = document.createElement("option"); + option.text = "-"; + [].slice.call(filter.options, 0).forEach(e => e.remove()); + filter.add(option); + var set = new Set(); + const rows = table.tBodies[0].rows; + for (var k = 0; k < rows.length; k++) { + const txt = rows[k].cells[idx].innerText; + if (txt) set.add(txt); + } + Array.from(set).sort().forEach(item => { + var option = document.createElement("option"); + option.text = item; + filter.add(option); + }); + } +} +function matchFilter(row, filters) { + for (var i = 0; i < filters.length; i++) { + var filter = filters[i]; + if (filter.value != "-") { + if (row.cells[filter.getAttribute('data-idx')].innerText != filter.value) { + return false; + } + } + } + return true; +} +function applyFilters(filter) { + var table = findElementRecursive(filter, 'TABLE') + var filters = table.getElementsByTagName("SELECT"); + var tBody = table.tBodies[0]; + var spareBody = table.tBodies[1]; + + var rows = [].slice.call(tBody.rows, 0).concat([].slice.call(spareBody.rows)); + + var hits = []; + var misses = []; + // Fill it with the sorted values + while (rows.length) { + var row = rows.splice(0, 1)[0]; + if (matchFilter(row, filters)) { + hits.push(row); + } else { + misses.push(row); + } + } + + var clone_tbody = tBody.cloneNode() + var clone_sparebody = spareBody.cloneNode() + while (hits.length) { + clone_tbody.appendChild(hits.splice(0, 1)[0]) + } + while (misses.length) { + clone_sparebody.appendChild(misses.splice(0, 1)[0]) + } + + table.replaceChild(clone_tbody, tBody) + table.replaceChild(clone_sparebody, spareBody) + +} + +function initFilter(filter) { + var table = findElementRecursive(filter, 'TABLE') + var cell = findElementRecursive(filter, 'TH') + var tr = findElementRecursive(filter, 'TR') + var idx = getIndex(tr.cells, cell) + + var spare = document.createElement("table"); + spare.style="display:none"; + cell.appendChild(spare); + filter.onchange = evt => applyFilters(evt.target) + filter.setAttribute('data-idx', idx) + + var option = document.createElement("option"); + option.text = "-"; + [].slice.call(filter.options, 0).forEach(e => e.remove()); + filter.add(option); + var set = new Set(); + const rows = table.tBodies[0].rows; + for (var k = 0; k < rows.length; k++) { + const txt = rows[k].cells[idx].innerText; + if (txt) set.add(txt); + } + Array.from(set).sort().forEach(item => { + var option = document.createElement("option"); + option.text = item; + filter.add(option); + }); +} + +var filters = document.getElementsByTagName("SELECT") +for (var i = 0; i < filters.length; i++) initFilter(filters[i]); +})(); + +/** + * used to communicate with 'configure' endpoint + */ +function updateValue(element) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (this.readyState == 4) { + if (this.status == 200) { + if (this.responseText == "OK") { + return; + } + if (this.responseText == "REFRESH") { + if (confirm("Action executed successfully. Reload pages?")) { + document.location.reload(); + } + return; + } + } + alert("An error occured: " + this.responseText); + } + }; + xmlhttp.open("POST", "configure", false); + xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + var value = element.value; + if (element.type == 'checkbox') { + value = element.checked ? 1 : 0; + } + xmlhttp.send(JSON.stringify([{'name' : element.name , 'value' : value}])); + +} diff --git a/ebean-core/src/main/resources/io/ebeaninternal/server/query/QueryPlanLoggerDb2.sql b/ebean-core/src/main/resources/io/ebeaninternal/server/query/QueryPlanLoggerDb2.sql new file mode 100644 index 0000000000..ae3993082c --- /dev/null +++ b/ebean-core/src/main/resources/io/ebeaninternal/server/query/QueryPlanLoggerDb2.sql @@ -0,0 +1,211 @@ +WITH tree(operator_ID, level, path, explain_time, cycle) +AS +( +SELECT 1 operator_id + , 0 level + , CAST('001' AS VARCHAR(1000)) path + , max(explain_time) explain_time + , 0 + FROM ${SCHEMA}.EXPLAIN_OPERATOR O + WHERE O.EXPLAIN_REQUESTER = SESSION_USER + +UNION ALL + +SELECT s.source_id + , level + 1 + , tree.path || '/' || LPAD(CAST(s.source_id AS VARCHAR(3)), 3, '0') path + , tree.explain_time + , POSITION('/' || LPAD(CAST(s.source_id AS VARCHAR(3)), 3, '0') || '/' IN path USING OCTETS) + FROM tree + , ${SCHEMA}.EXPLAIN_STREAM S + WHERE s.target_id = tree.operator_id + AND s.explain_time = tree.explain_time + AND S.Object_Name IS NULL + AND S.explain_requester = SESSION_USER + AND tree.cycle = 0 + AND level < 100 +) +SELECT * + FROM ( +SELECT "Explain Plan" + FROM ( +SELECT CAST( LPAD(id, MAX(LENGTH(id)) OVER(), ' ') + || ' | ' + || RPAD(operation, MAX(LENGTH(operation)) OVER(), ' ') + || ' | ' + || LPAD(rows, MAX(LENGTH(rows)) OVER(), ' ') + || ' | ' + -- Don't show ActualRows columns if there are no actuals available at all + || CASE WHEN COUNT(ActualRows) OVER () > 1 -- the heading 'ActualRows' is always present, so "1" means no OTHER values + THEN LPAD(ActualRows, MAX(LENGTH(ActualRows)) OVER(), ' ') || ' | ' + ELSE '' + END + || LPAD(cost, MAX(LENGTH(cost)) OVER(), ' ') + AS VARCHAR(100)) "Explain Plan" + , path + FROM ( +SELECT 'ID' ID + , 'Operation' Operation + , 'Rows' Rows + , 'ActualRows' ActualRows + , 'Cost' Cost + , '0' Path + FROM SYSIBM.SYSDUMMY1 +-- TODO: UNION ALL yields duplicate. where do they come from? +UNION +SELECT CAST(tree.operator_id as VARCHAR(254)) ID + , CAST(LPAD(' ', tree.level, ' ') + || CASE WHEN tree.cycle = 1 + THEN '(cycle) ' + ELSE '' + END + || COALESCE ( + TRIM(O.Operator_Type) + || COALESCE(' (' || argument || ')', '') + || ' ' + || COALESCE(S.Object_Name,'') + , '' + ) + AS VARCHAR(254)) AS OPERATION + , COALESCE(CAST(rows AS VARCHAR(254)), '') Rows + , CAST(ActualRows as VARCHAR(254)) ActualRows -- note: no coalesce + , COALESCE(CAST(CAST(O.Total_Cost AS BIGINT) AS VARCHAR(254)), '') Cost + , path + FROM tree + LEFT JOIN ( SELECT i.source_id + , i.target_id + , CAST(CAST(ROUND(o.stream_count) AS BIGINT) AS VARCHAR(12)) + || ' of ' + || CAST (total_rows AS VARCHAR(12)) + || CASE WHEN total_rows > 0 + AND ROUND(o.stream_count) <= total_rows THEN + ' (' + || LPAD(CAST (ROUND(ROUND(o.stream_count)/total_rows*100,2) + AS NUMERIC(5,2)), 6, ' ') + || '%)' + ELSE '' + END rows + , CASE WHEN act.actual_value is not null then + CAST(CAST(ROUND(act.actual_value) AS BIGINT) AS VARCHAR(12)) + || ' of ' + || CAST (total_rows AS VARCHAR(12)) + || CASE WHEN total_rows > 0 THEN + ' (' + || LPAD(CAST (ROUND(ROUND(act.actual_value)/total_rows*100,2) + AS NUMERIC(5,2)), 6, ' ') + || '%)' + ELSE NULL + END END ActualRows + , i.object_name + , i.explain_time + FROM (SELECT MAX(source_id) source_id + , target_id + , MIN(CAST(ROUND(stream_count,0) AS BIGINT)) total_rows + , CAST(LISTAGG(object_name) AS VARCHAR(50)) object_name + , explain_time + FROM ${SCHEMA}.EXPLAIN_STREAM + WHERE explain_time = (SELECT MAX(explain_time) + FROM ${SCHEMA}.EXPLAIN_OPERATOR + WHERE EXPLAIN_REQUESTER = SESSION_USER + ) + GROUP BY target_id, explain_time + ) I + LEFT JOIN ${SCHEMA}.EXPLAIN_STREAM O + ON ( I.target_id=o.source_id + AND I.explain_time = o.explain_time + AND O.EXPLAIN_REQUESTER = SESSION_USER + ) + LEFT JOIN ${SCHEMA}.EXPLAIN_ACTUALS act + ON ( act.operator_id = i.target_id + AND act.explain_time = i.explain_time + AND act.explain_requester = SESSION_USER + AND act.ACTUAL_TYPE like 'CARDINALITY%' + ) + ) s + ON ( s.target_id = tree.operator_id + AND s.explain_time = tree.explain_time + ) + LEFT JOIN ${SCHEMA}.EXPLAIN_OPERATOR O + ON ( o.operator_id = tree.operator_id + AND o.explain_time = tree.explain_time + AND o.explain_requester = SESSION_USER + ) + LEFT JOIN (SELECT LISTAGG (CASE argument_type + WHEN 'UNIQUE' THEN + CASE WHEN argument_value = 'TRUE' + THEN 'UNIQUE' + ELSE NULL + END + WHEN 'TRUNCSRT' THEN + CASE WHEN argument_value = 'TRUE' + THEN 'TOP-N' + ELSE NULL + END + WHEN 'SCANDIR' THEN + CASE WHEN argument_value != 'FORWARD' + THEN argument_value + ELSE NULL + END + ELSE argument_value + END + , ' ') argument + , operator_id + , explain_time + FROM ${SCHEMA}.EXPLAIN_ARGUMENT EA + WHERE argument_type IN ('AGGMODE' -- GRPBY + , 'UNIQUE', 'TRUNCSRT' -- SORT + , 'SCANDIR' -- IXSCAN, TBSCAN + , 'OUTERJN' -- JOINs + ) + AND explain_requester = SESSION_USER + GROUP BY explain_time, operator_id + + ) A + ON ( a.operator_id = tree.operator_id + AND a.explain_time = tree.explain_time + ) + ) O +UNION ALL +VALUES ('Explain plan (c) 2014-2017 by Markus Winand - NO WARRANTY - V20171102','Z0') + , ('Modifications by Ember Crooks - NO WARRANTY','Z1') + , ('http://use-the-index-luke.com/s/last_explained','Z2') + , ('', 'A') + , ('', 'Y') + , ('Predicate Information', 'AA') +UNION ALL +SELECT CAST (LPAD(CASE WHEN operator_id = LAG (operator_id) + OVER (PARTITION BY operator_id + ORDER BY pred_order + ) + THEN '' + ELSE operator_id || ' - ' + END + , MAX(LENGTH(operator_id )+4) OVER() + , ' ') + || how_applied + || ' ' + || predicate_text + AS VARCHAR(100)) "Predicate Information" + , 'P' || LPAD(id_order, 5, '0') || pred_order path + FROM (SELECT CAST(operator_id AS VARCHAR(254)) operator_id + , LPAD(trim(how_applied) + , MAX (LENGTH(TRIM(how_applied))) + OVER (PARTITION BY operator_id) + , ' ' + ) how_applied + -- next: capped to length 80 to avoid + -- SQL0445W Value "..." has been truncated. SQLSTATE=01004 + -- error when long literal values may appear (space padded!) + , CAST(substr(predicate_text, 1, 80) AS VARCHAR(80)) predicate_text + , CASE how_applied WHEN 'START' THEN '1' + WHEN 'STOP' THEN '2' + WHEN 'SARG' THEN '3' + ELSE '9' + END pred_order + , operator_id id_order + FROM ${SCHEMA}.EXPLAIN_PREDICATE p + WHERE explain_time = (SELECT max(explain_time) FROM ${SCHEMA}.EXPLAIN_STATEMENT WHERE queryno = ?) + ) +) +ORDER BY path +) diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/core/HtmlMetricTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/core/HtmlMetricTest.java new file mode 100644 index 0000000000..35b1ebddd3 --- /dev/null +++ b/ebean-core/src/test/java/io/ebeaninternal/server/core/HtmlMetricTest.java @@ -0,0 +1,26 @@ +package io.ebeaninternal.server.core; + +import io.ebean.DB; +import io.ebean.meta.MetricReportGenerator; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HtmlMetricTest { + + + @Test + public void testReport() throws IOException { + MetricReportGenerator generator = DB.getDefault().metaInfo().createReportGenerator(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + generator.writeReport(baos); + + assertThat(baos.toString(StandardCharsets.UTF_8)).contains(""); + } +} diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/deploy/BeanDescriptorTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/deploy/BeanDescriptorTest.java index 89f77d70b6..9efddfaace 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/deploy/BeanDescriptorTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/deploy/BeanDescriptorTest.java @@ -135,7 +135,7 @@ public void merge_when_empty() { from.setName("rob"); Customer to = new Customer(); - customerDesc.merge((EntityBean) from, (EntityBean) to); + customerDesc.mergeBeans((EntityBean) from, (EntityBean) to, null); assertThat(to.getId()).isEqualTo(42); assertThat(to.getName()).isEqualTo("rob"); diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/BasePlatformArrayTypeFactoryTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/BasePlatformArrayTypeFactoryTest.java index 4b44364875..36503a9545 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/BasePlatformArrayTypeFactoryTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/BasePlatformArrayTypeFactoryTest.java @@ -56,7 +56,7 @@ static class TestDoubleDataBind extends DataBind { boolean wasPgoEmpty; TestDoubleDataBind() { - super(null, null, null); + super(null, 0,null, null); } @Override diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/DefaultTypeManagerTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/DefaultTypeManagerTest.java index b3bdc17db3..2fd51c0d3b 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/DefaultTypeManagerTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/DefaultTypeManagerTest.java @@ -57,7 +57,7 @@ public void enumDayMonth_builtIn_overrideAsString() { assertThat(true).isFalse().as("never get here"); } catch (IllegalStateException e) { - assertThat(e.getMessage()).contains("It is mapped using 2 different modes when only one is supported"); + assertThat(e.getMessage()).contains("It is mapped using 2 of (ORDINAL, STRING or an Ebean mapping) when only one is supported."); } } @@ -72,7 +72,7 @@ public void enumMonth_builtIn_overrideAsOrdinal() { typeManager.enumType(Month.class, EnumType.STRING); assertThat(true).isFalse().as("never get here"); } catch (IllegalStateException e) { - assertThat(e.getMessage()).contains("It is mapped using 2 different modes when only one is supported"); + assertThat(e.getMessage()).contains("It is mapped using 2 of (ORDINAL, STRING or an Ebean mapping) when only one is supported."); } } @@ -90,7 +90,7 @@ public void enumDayOfWeek_builtIn_overrideAsString() { typeManager.enumType(DayOfWeek.class, EnumType.ORDINAL); assertThat(true).isFalse().as("never get here"); } catch (IllegalStateException e) { - assertThat(e.getMessage()).contains("It is mapped using 2 different modes when only one is supported"); + assertThat(e.getMessage()).contains("It is mapped using 2 of (ORDINAL, STRING or an Ebean mapping) when only one is supported."); } } @@ -105,7 +105,7 @@ public void enumDayOfWeek_builtIn_overrideAsOrdinal() { typeManager.enumType(DayOfWeek.class, EnumType.STRING); assertThat(true).isFalse().as("never get here"); } catch (IllegalStateException e) { - assertThat(e.getMessage()).contains("It is mapped using 2 different modes when only one is supported"); + assertThat(e.getMessage()).contains("It is mapped using 2 of (ORDINAL, STRING or an Ebean mapping) when only one is supported."); } } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeIntegerTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeIntegerTest.java index ba292a20a6..b72e3306b9 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeIntegerTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeIntegerTest.java @@ -1,8 +1,13 @@ package io.ebeaninternal.server.type; +import com.fasterxml.jackson.core.JsonParser; +import io.ebean.DB; import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.io.StringReader; + import static org.assertj.core.api.Assertions.assertThat; public class ScalarTypeIntegerTest { @@ -19,4 +24,18 @@ public void format_when_integer() { assertThat(type.format(1)).isEqualTo("1"); } + @Test + public void json() throws IOException { + + JsonParser parser = DB.json().createParser(new StringReader("1")); + parser.nextToken(); + Object parsed = type.jsonRead(parser); + assertThat(parsed).isEqualTo(1); + + parser = DB.json().createParser(new StringReader("\"1.0\"")); + parser.nextToken(); + parsed = type.jsonRead(parser); + assertThat(parsed).isEqualTo(1); + } + } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java index f645ab3cba..8b0498b881 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java @@ -144,7 +144,7 @@ public void testParseEbean11() throws IOException { p = type.parse("2022-01-01T01:00:00"); parser13.nextToken(); - q = type.jsonRead(parser13); + q = type.jsonRead(parser13); assertThat(p).isEqualTo(q); TimeZone tz = TimeZone.getDefault(); try { diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/TestTypeManager.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/TestTypeManager.java index f99991ebd9..ac94509cca 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/TestTypeManager.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/TestTypeManager.java @@ -57,7 +57,7 @@ void testEnumWithSubclasses() throws SQLException { typeManager.enumType(MyEnum.class, EnumType.STRING); fail("never get here"); } catch (IllegalStateException e) { - assertThat(e.getMessage()).contains("It is mapped using 2 different modes when only one is supported"); + assertThat(e.getMessage()).contains("It is mapped using 2 of (ORDINAL, STRING or an Ebean mapping) when only one is supported."); } } @@ -91,7 +91,7 @@ void testEnumWithChar() throws SQLException { typeManager.enumType(MyDayOfWeek.class, EnumType.ORDINAL); fail("never get here"); } catch (IllegalStateException e) { - assertThat(e.getMessage()).contains("It is mapped using 2 different modes when only one is supported"); + assertThat(e.getMessage()).contains("It is mapped using 2 of (ORDINAL, STRING or an Ebean mapping) when only one is supported."); } } diff --git a/ebean-csv-reader/pom.xml b/ebean-csv-reader/pom.xml index 970371e14e..8378beb97a 100644 --- a/ebean-csv-reader/pom.xml +++ b/ebean-csv-reader/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-csv-reader</artifactId> @@ -14,21 +14,21 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-test</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> diff --git a/ebean-ddl-generator/pom.xml b/ebean-ddl-generator/pom.xml index defeaae195..066f379cb8 100644 --- a/ebean-ddl-generator/pom.xml +++ b/ebean-ddl-generator/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <name>ebean ddl generation</name> @@ -28,14 +28,14 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core-type</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> @@ -72,7 +72,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-all</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java new file mode 100644 index 0000000000..1839cf4917 --- /dev/null +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java @@ -0,0 +1,56 @@ +package io.ebeaninternal.dbmigration; + +import java.io.IOException; + +import io.ebean.plugin.Plugin; +import io.ebean.plugin.SpiServer; + +/** + * Plugin to generate db-migration scripts automatically. + * @author Roland Praml, FOCONIS AG + */ +public class DbMigrationPlugin implements Plugin { + + private DefaultDbMigration dbMigration; + + private static String lastMigration; + private static String lastInit; + + @Override + public void configure(SpiServer server) { + dbMigration = new DefaultDbMigration(); + dbMigration.setServer(server); + } + + @Override + public void online(boolean online) { + try { + lastInit = null; + lastMigration = null; + if (dbMigration.generate) { + String tmp = lastMigration = dbMigration.generateMigration(); + if (tmp == null) { + return; + } + } + if (dbMigration.generateInit) { + lastInit = dbMigration.generateInitMigration(); + } + } catch (IOException e) { + throw new RuntimeException("Error while generating migration", e); + } + } + + @Override + public void shutdown() { + dbMigration = null; + } + + public static String getLastInit() { + return lastInit; + } + + public static String getLastMigration() { + return lastMigration; + } +} diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java index 7567ad4cf4..c0c56abddf 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java @@ -61,13 +61,12 @@ public DdlGenerator(SpiEbeanServer server) { this.platform = databasePlatform.platform(); this.platformName = platform.base().name(); if (!config.getTenantMode().isDdlEnabled() && config.isDdlRun()) { - log.log(WARNING, "DDL can't be run on startup with TenantMode " + config.getTenantMode()); + log.log(WARNING, "DDL can''t be run on startup with TenantMode " + config.getTenantMode()); this.runDdl = false; - this.useMigrationStoredProcedures = false; } else { this.runDdl = config.isDdlRun(); - this.useMigrationStoredProcedures = config.getDatabasePlatform().useMigrationStoredProcedures(); } + this.useMigrationStoredProcedures = config.getDatabasePlatform() != null && config.getDatabasePlatform().useMigrationStoredProcedures(); this.scriptTransform = createScriptTransform(config); this.baseDir = initBaseDir(); } @@ -85,7 +84,7 @@ private File initBaseDir() { @Override public void execute(boolean online) { generateDdl(); - if (online) { + if (online && runDdl) { runDdl(); } } @@ -105,16 +104,15 @@ protected void generateDdl() { /** * Run the DDL drop and DDL create scripts if properties have been set. */ - protected void runDdl() { - if (runDdl) { - Connection connection = null; - try { - connection = obtainConnection(); - runDdlWith(connection); - } finally { - JdbcClose.rollback(connection); - JdbcClose.close(connection); - } + @Override + public void runDdl() { + Connection connection = null; + try { + connection = obtainConnection(); + runDdlWith(connection); + } finally { + JdbcClose.rollback(connection); + JdbcClose.close(connection); } } diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java index a96957d0ab..23bc87c923 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java @@ -1,17 +1,16 @@ package io.ebeaninternal.dbmigration; import io.avaje.applog.AppLog; +import io.avaje.classpath.scanner.core.Location; import io.ebean.DB; import io.ebean.Database; import io.ebean.annotation.Platform; -import io.ebean.config.DatabaseConfig; -import io.ebean.config.DbConstraintNaming; -import io.ebean.config.PlatformConfig; -import io.ebean.config.PropertiesWrapper; +import io.ebean.config.*; import io.ebean.config.dbplatform.DatabasePlatform; import io.ebean.config.dbplatform.DatabasePlatformProvider; import io.ebean.dbmigration.DbMigration; import io.ebean.util.IOUtils; +import io.ebean.util.StringHelper; import io.ebeaninternal.api.DbOffline; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.dbmigration.ddlgeneration.DdlOptions; @@ -26,10 +25,7 @@ import java.io.File; import java.io.IOException; import java.io.Writer; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; -import java.util.ServiceLoader; +import java.util.*; import static io.ebeaninternal.api.PlatformMatch.matchPlatform; import static java.lang.System.Logger.Level.*; @@ -61,8 +57,8 @@ public class DefaultDbMigration implements DbMigration { private static final String initialVersion = "1.0"; private static final String GENERATED_COMMENT = "THIS IS A GENERATED FILE - DO NOT MODIFY"; - private final List<DatabasePlatformProvider> platformProviders = new ArrayList<>(); - protected final boolean online; + private List<DatabasePlatformProvider> platformProviders = new ArrayList<>(); + protected boolean online; private boolean logToSystemOut = true; protected SpiEbeanServer server; protected String pathToResources = "src/main/resources"; @@ -77,8 +73,10 @@ public class DefaultDbMigration implements DbMigration { protected List<Pair> platforms = new ArrayList<>(); protected DatabaseConfig databaseConfig; protected DbConstraintNaming constraintNaming; + @Deprecated protected Boolean strictMode; - protected Boolean includeGeneratedFileComment; + protected boolean includeGeneratedFileComment; + @Deprecated protected String header; protected String applyPrefix = ""; protected String version; @@ -88,6 +86,9 @@ public class DefaultDbMigration implements DbMigration { private int lockTimeoutSeconds; protected boolean includeBuiltInPartitioning = true; protected boolean includeIndex; + protected boolean generate = false; + protected boolean generateInit = false; + private boolean keepLastInit = true; /** * Create for offline migration generation. @@ -123,12 +124,66 @@ public void setServerConfig(DatabaseConfig config) { if (constraintNaming == null) { this.constraintNaming = databaseConfig.getConstraintNaming(); } + if (databasePlatform == null) { + this.databasePlatform = databaseConfig.getDatabasePlatform(); + } Properties properties = config.getProperties(); if (properties != null) { - PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, null); + PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, config.getClassLoadConfig()); migrationPath = props.get("migration.migrationPath", migrationPath); migrationInitPath = props.get("migration.migrationInitPath", migrationInitPath); pathToResources = props.get("migration.pathToResources", pathToResources); + addForeignKeySkipCheck = props.getBoolean("migration.addForeignKeySkipCheck", addForeignKeySkipCheck); + applyPrefix = props.get("migration.applyPrefix", applyPrefix); + databasePlatform = props.createInstance(DatabasePlatform.class, "migration.databasePlatform", databasePlatform); + generatePendingDrop = props.get("migration.generatePendingDrop", generatePendingDrop); + includeBuiltInPartitioning = props.getBoolean("migration.includeBuiltInPartitioning", includeBuiltInPartitioning); + includeGeneratedFileComment = props.getBoolean("migration.includeGeneratedFileComment", includeGeneratedFileComment); + includeIndex = props.getBoolean("migration.includeIndex", includeIndex); + lockTimeoutSeconds = props.getInt("migration.lockTimeoutSeconds", lockTimeoutSeconds); + logToSystemOut = props.getBoolean("migration.logToSystemOut", logToSystemOut); + modelPath = props.get("migration.modelPath", modelPath); + modelSuffix = props.get("migration.modelSuffix", modelSuffix); + name = props.get("migration.name", name); + online = props.getBoolean("migration.online", online); + vanillaPlatform = props.getBoolean("migration.vanillaPlatform", vanillaPlatform); + version = props.get("migration.version", version); + generate = props.getBoolean("migration.generate", generate); + generateInit = props.getBoolean("migration.generateInit", generateInit); + // header & strictMode must be configured at DatabaseConfig level + parsePlatforms(props, config); + } + } + + protected void parsePlatforms(PropertiesWrapper props, DatabaseConfig config) { + String platforms = props.get("migration.platforms"); + if (platforms == null || platforms.isEmpty()) { + return; + } + String[] tmp = StringHelper.splitNames(platforms); + for (String plat : tmp) { + DatabasePlatform dbPlatform; + String platformName = plat; + String platformPrefix = null; + int pos = plat.indexOf('='); + if (pos != -1) { + platformName = plat.substring(0, pos); + platformPrefix = plat.substring(pos + 1); + } + + if (platformName.indexOf('.') == -1) { + // parse platform as enum value + Platform platform = Enum.valueOf(Platform.class, platformName.toUpperCase()); + dbPlatform = platform(platform); + } else { + // parse platform as class + dbPlatform = (DatabasePlatform) config.getClassLoadConfig().newInstance(platformName); + } + if (platformPrefix == null) { + platformPrefix = dbPlatform.platform().name().toLowerCase(); + } + + addDatabasePlatform(dbPlatform, platformPrefix); } } @@ -319,7 +374,18 @@ private String generateMigrationFor(boolean initMigration) throws IOException { } String pendingVersion = generatePendingDrop(); - if (pendingVersion != null) { + if ("auto".equals(pendingVersion)) { + StringJoiner sj = new StringJoiner(","); + String diff = generateDiff(request); + if (diff != null) { + sj.add(diff); + request = createRequest(initMigration); + } + for (String pendingDrop : request.getPendingDrops()) { + sj.add(generatePendingDrop(request, pendingDrop)); + } + return sj.length() == 0 ? null : sj.toString(); + } else if (pendingVersion != null) { return generatePendingDrop(request, pendingVersion); } else { return generateDiff(request); @@ -376,6 +442,7 @@ private void configurePlatforms() { private void generateExtraDdl(File migrationDir, DatabasePlatform dbPlatform, boolean tablePartitioning) throws IOException { if (dbPlatform != null) { if (tablePartitioning && includeBuiltInPartitioning) { + generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.readBuiltinTablePartitioning(), false); } // skip built-in migration stored procedures based on isUseMigrationStoredProcedures @@ -384,6 +451,7 @@ private void generateExtraDdl(File migrationDir, DatabasePlatform dbPlatform, bo } } + private void generateExtraDdlFor(File migrationDir, DatabasePlatform dbPlatform, ExtraDdl extraDdl, boolean checkSkip) throws IOException { if (extraDdl != null) { List<DdlScript> ddlScript = extraDdl.getDdlScript(); @@ -554,7 +622,7 @@ private String generateMigration(Request request, Migration dbMigration, String return null; } else { if (!platforms.isEmpty()) { - writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir); + writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir, request.initMigration && keepLastInit); } else if (databasePlatform != null) { // writer needs the current model to provide table/column details for @@ -634,12 +702,17 @@ private String toUnderScore(String name) { /** * Write any extra platform ddl. */ - private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException { + private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath, boolean clear) throws IOException { DdlOptions options = new DdlOptions(addForeignKeySkipCheck); for (Pair pair : platforms) { DdlWrite writer = new DdlWrite(new MConfiguration(), currentModel.read(), options); PlatformDdlWriter platformWriter = createDdlWriter(pair.platform); File subPath = platformWriter.subPath(writePath, pair.prefix); + if (clear) { + for (File existing : subPath.listFiles()) { + existing.delete(); + } + } platformWriter.processMigration(dbMigration, writer, subPath, fullVersion); } } @@ -657,7 +730,7 @@ private boolean writeMigrationXml(Migration dbMigration, File resourcePath, Stri if (file.exists()) { return false; } - String comment = Boolean.TRUE.equals(includeGeneratedFileComment) ? GENERATED_COMMENT : null; + String comment = includeGeneratedFileComment ? GENERATED_COMMENT : null; MigrationXmlWriter xmlWriter = new MigrationXmlWriter(comment); xmlWriter.write(dbMigration, file); return true; @@ -675,6 +748,7 @@ private void setDefaults() { databasePlatform = server.databasePlatform(); } if (databaseConfig != null) { + // FIXME: Copy header and StrictMode to databaseConfig if (strictMode != null) { databaseConfig.setDdlStrictMode(strictMode); } @@ -749,15 +823,20 @@ public File migrationDirectory() { * Return the file path to write the xml and sql to. */ File migrationDirectory(boolean initMigration) { - // path to src/main/resources in typical maven project - File resourceRootDir = new File(pathToResources); - if (!resourceRootDir.exists()) { - String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); - throw new UnknownResourcePathException(msg); - } - String resourcePath = migrationPath(initMigration); + Location resourcePath = migrationPath(initMigration); // expect to be a path to something like - src/main/resources/dbmigration - File path = new File(resourceRootDir, resourcePath); + File path; + if (resourcePath.isClassPath()) { + // path to src/main/resources in typical maven project + File resourceRootDir = new File(pathToResources); + if (!resourceRootDir.exists()) { + String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); + throw new UnknownResourcePathException(msg); + } + path = new File(resourceRootDir, resourcePath.path()); + } else { + path = new File(resourcePath.path()); + } if (!path.exists()) { if (!path.mkdirs()) { logInfo("Warning - Unable to ensure migration directory exists at %s", path.getAbsolutePath()); @@ -766,8 +845,9 @@ File migrationDirectory(boolean initMigration) { return path; } - private String migrationPath(boolean initMigration) { - return initMigration ? migrationInitPath : migrationPath; + private Location migrationPath(boolean initMigration) { + // remove classpath: or filesystem: prefix + return new Location(initMigration ? migrationInitPath : migrationPath); } /** diff --git a/ebean-ddl-generator/src/main/java/module-info.java b/ebean-ddl-generator/src/main/java/module-info.java index 1a7ef332e4..c5b6af08b2 100644 --- a/ebean-ddl-generator/src/main/java/module-info.java +++ b/ebean-ddl-generator/src/main/java/module-info.java @@ -1,5 +1,6 @@ module io.ebean.ddl.generator { + uses io.ebean.plugin.Plugin; exports io.ebean.dbmigration; provides io.ebean.dbmigration.DbMigration with io.ebeaninternal.dbmigration.DefaultDbMigration; diff --git a/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin b/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin new file mode 100644 index 0000000000..83ec94df48 --- /dev/null +++ b/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin @@ -0,0 +1 @@ +io.ebeaninternal.dbmigration.DbMigrationPlugin diff --git a/ebean-ddl-generator/src/test/resources/application-test.properties b/ebean-ddl-generator/src/test/resources/application-test.properties index dfcb052ab0..af973bb3a5 100644 --- a/ebean-ddl-generator/src/test/resources/application-test.properties +++ b/ebean-ddl-generator/src/test/resources/application-test.properties @@ -5,32 +5,12 @@ datasource.default=h2 datasource.h2.username=sa datasource.h2.password= -datasource.h2.url=jdbc:h2:mem:h2AutoTune +datasource.h2.url=jdbc:h2:mem:h2AutoTune;NON_KEYWORDS=KEY,VALUE + +datasource.db2.username=migtest +datasource.db2.password=migtest +datasource.db2.url=jdbc:db2://localhost:50005/migtest datasource.pg.username=sa datasource.pg.password= datasource.pg.url=jdbc:h2:mem:h2AutoTune - -# parameters for migration test -datasource.migrationtest.username=SA -datasource.migrationtest.password=SA -datasource.migrationtest.url=jdbc:h2:mem:migration -ebean.migrationtest.applyPrefix=V -ebean.migrationtest.ddl.generate=false -ebean.migrationtest.ddl.run=false -ebean.migrationtest.ddl.header=-- Migrationscripts for ebean unittest -ebean.migrationtest.migration.appName=migrationtest -ebean.migrationtest.migration.migrationPath=dbmigration/migrationtest -ebean.migrationtest.migration.strict=true - -# parameters for migration test -datasource.migrationtest-history.username=SA -datasource.migrationtest-history.password=SA -datasource.migrationtest-history.url=jdbc:h2:mem:migration -ebean.migrationtest-history.applyPrefix=V -ebean.migrationtest-history.ddl.generate=false -ebean.migrationtest-history.ddl.run=false -ebean.migrationtest-history.ddl.header=-- Migrationscripts for ebean unittest DbMigrationDropHistoryTest -ebean.migrationtest-history.migration.appName=migrationtest-history -ebean.migrationtest-history.migration.migrationPath=dbmigration/migrationtest-history -ebean.migrationtest-history.migration.strict=true diff --git a/ebean-externalmapping-api/pom.xml b/ebean-externalmapping-api/pom.xml index 9564d2788a..19b95f4996 100644 --- a/ebean-externalmapping-api/pom.xml +++ b/ebean-externalmapping-api/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <name>ebean external mapping api</name> diff --git a/ebean-externalmapping-xml/pom.xml b/ebean-externalmapping-xml/pom.xml index 3e2458c869..ef2de76e28 100644 --- a/ebean-externalmapping-xml/pom.xml +++ b/ebean-externalmapping-xml/pom.xml @@ -4,12 +4,12 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <scm> - <developerConnection>scm:git:git@github.com:ebean-orm/ebean.git</developerConnection> - <tag>HEAD</tag> + <developerConnection>scm:git:git@github.com:FOCONIS/ebean.git</developerConnection> + <tag>ebean-parent-13.6.4-FOC1</tag> </scm> <name>ebean external mapping xml</name> @@ -21,7 +21,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-externalmapping-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <!-- JAVAX-DEPENDENCY-START --> @@ -68,21 +68,21 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-h2</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-ddl-generator</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> diff --git a/ebean-jackson-jsonnode/pom.xml b/ebean-jackson-jsonnode/pom.xml index 72772a8531..5f6689e845 100644 --- a/ebean-jackson-jsonnode/pom.xml +++ b/ebean-jackson-jsonnode/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-jackson-jsonnode</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core-type</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> diff --git a/ebean-jackson-mapper/pom.xml b/ebean-jackson-mapper/pom.xml index cd2d9918f0..211b99d429 100644 --- a/ebean-jackson-mapper/pom.xml +++ b/ebean-jackson-mapper/pom.xml @@ -3,7 +3,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core-type</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> diff --git a/ebean-joda-time/pom.xml b/ebean-joda-time/pom.xml index d1569fb4f5..cce59e0141 100644 --- a/ebean-joda-time/pom.xml +++ b/ebean-joda-time/pom.xml @@ -3,7 +3,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core-type</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> diff --git a/ebean-kotlin/pom.xml b/ebean-kotlin/pom.xml index e3fb55af88..0af9dae5c5 100644 --- a/ebean-kotlin/pom.xml +++ b/ebean-kotlin/pom.xml @@ -6,7 +6,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-kotlin</artifactId> @@ -28,7 +28,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>${project.version}</version> <scope>provided</scope> </dependency> @@ -50,7 +50,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-test</artifactId> - <version>13.17.3</version> + <version>${project.version}</version> <scope>test</scope> </dependency> diff --git a/ebean-postgis/pom.xml b/ebean-postgis/pom.xml index ee6b8eb125..9215f85a45 100644 --- a/ebean-postgis/pom.xml +++ b/ebean-postgis/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <name>ebean postgis</name> @@ -22,14 +22,14 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-postgres</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <!-- provided scope --> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> @@ -65,7 +65,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-test</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> diff --git a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java index 6a825a8b64..243b8cba76 100644 --- a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java +++ b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java @@ -98,12 +98,7 @@ public T toBeanType(Object value) { @Override public String formatValue(T value) { - return null; - } - - @Override - public T parse(String value) { - return null; + return value.toString(); } @Override diff --git a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisLineString.java b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisLineString.java index ac4b010d1c..33e843a17a 100644 --- a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisLineString.java +++ b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisLineString.java @@ -3,10 +3,20 @@ import io.ebean.config.dbplatform.ExtraDbTypes; import org.postgis.LineString; +import java.sql.SQLException; + public class ScalarTypePgisLineString extends ScalarTypePgisBase<LineString> { public ScalarTypePgisLineString() { super(ExtraDbTypes.LINESTRING, LineString.class); } + @Override + public LineString parse(String value) { + try { + return new LineString(value); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } } diff --git a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiLineString.java b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiLineString.java index e352f0c7d3..8ddf0505bb 100644 --- a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiLineString.java +++ b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiLineString.java @@ -3,10 +3,20 @@ import io.ebean.config.dbplatform.ExtraDbTypes; import org.postgis.MultiLineString; +import java.sql.SQLException; + public class ScalarTypePgisMultiLineString extends ScalarTypePgisBase<MultiLineString> { public ScalarTypePgisMultiLineString() { super(ExtraDbTypes.MULTILINESTRING, MultiLineString.class); } + @Override + public MultiLineString parse(String value) { + try { + return new MultiLineString(value); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } } diff --git a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiPoint.java b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiPoint.java index 9b2a2ec6ed..2e88d5564d 100644 --- a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiPoint.java +++ b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiPoint.java @@ -3,10 +3,20 @@ import io.ebean.config.dbplatform.ExtraDbTypes; import org.postgis.MultiPoint; +import java.sql.SQLException; + public class ScalarTypePgisMultiPoint extends ScalarTypePgisBase<MultiPoint> { public ScalarTypePgisMultiPoint() { super(ExtraDbTypes.MULTIPOINT, MultiPoint.class); } + @Override + public MultiPoint parse(String value) { + try { + return new MultiPoint(value); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } } diff --git a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiPolygon.java b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiPolygon.java index eca6f2bcdb..f4d590ef47 100644 --- a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiPolygon.java +++ b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisMultiPolygon.java @@ -3,10 +3,20 @@ import io.ebean.config.dbplatform.ExtraDbTypes; import org.postgis.MultiPolygon; +import java.sql.SQLException; + public class ScalarTypePgisMultiPolygon extends ScalarTypePgisBase<MultiPolygon> { public ScalarTypePgisMultiPolygon() { super(ExtraDbTypes.MULTIPOLYGON, MultiPolygon.class); } + @Override + public MultiPolygon parse(String value) { + try { + return new MultiPolygon(value); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } } diff --git a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisPoint.java b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisPoint.java index 69f1e44141..6a61607ede 100644 --- a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisPoint.java +++ b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisPoint.java @@ -3,10 +3,20 @@ import io.ebean.config.dbplatform.ExtraDbTypes; import org.postgis.Point; +import java.sql.SQLException; + public class ScalarTypePgisPoint extends ScalarTypePgisBase<Point> { public ScalarTypePgisPoint() { super(ExtraDbTypes.POINT, Point.class); } + @Override + public Point parse(String value) { + try { + return new Point(value); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } } diff --git a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisPolygon.java b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisPolygon.java index d6cc70f2cb..10cefde441 100644 --- a/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisPolygon.java +++ b/ebean-postgis/src/main/java/io/ebean/postgis/ScalarTypePgisPolygon.java @@ -3,9 +3,20 @@ import io.ebean.config.dbplatform.ExtraDbTypes; import org.postgis.Polygon; +import java.sql.SQLException; + public class ScalarTypePgisPolygon extends ScalarTypePgisBase<Polygon> { public ScalarTypePgisPolygon() { super(ExtraDbTypes.POLYGON, Polygon.class); } + + @Override + public Polygon parse(String value) { + try { + return new Polygon(value); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } } diff --git a/ebean-querybean/pom.xml b/ebean-querybean/pom.xml index 046272af1d..8a4747180f 100644 --- a/ebean-querybean/pom.xml +++ b/ebean-querybean/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <name>ebean querybean</name> @@ -17,7 +17,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> @@ -63,21 +63,21 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-ddl-generator</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>querybean-generator</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-test</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> diff --git a/ebean-redis/pom.xml b/ebean-redis/pom.xml index 8d88985536..7363cb350a 100644 --- a/ebean-redis/pom.xml +++ b/ebean-redis/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-redis</artifactId> @@ -22,35 +22,35 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-querybean</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>querybean-generator</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-test</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> diff --git a/ebean-spring-txn/pom.xml b/ebean-spring-txn/pom.xml index 2c19788439..4fe618bef3 100644 --- a/ebean-spring-txn/pom.xml +++ b/ebean-spring-txn/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-spring-txn</artifactId> @@ -28,7 +28,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> @@ -77,7 +77,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-test</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> diff --git a/ebean-test/pom.xml b/ebean-test/pom.xml index feb87cb69a..069191dec8 100644 --- a/ebean-test/pom.xml +++ b/ebean-test/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <name>ebean test</name> @@ -28,20 +28,20 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-h2</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-ddl-generator</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> @@ -118,28 +118,28 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-joda-time</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-jackson-jsonnode</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-jackson-mapper</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-all</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> @@ -292,7 +292,7 @@ <build> <plugins> - <plugin> +<!-- <plugin> <groupId>io.repaint.maven</groupId> <artifactId>tiles-maven-plugin</artifactId> <version>2.34</version> @@ -302,8 +302,32 @@ <tile>io.ebean.tile:enhancement:13.17.1</tile> </tiles> </configuration> + </plugin>--> + <plugin> + <groupId>io.ebean</groupId> + <artifactId>ebean-maven-plugin</artifactId> + <version>${ebean-maven-plugin.version}</version> + <executions> + <execution> + <id>test</id> + <phase>process-test-classes</phase> + <configuration> + <transformArgs>debug=9</transformArgs> + </configuration> + <goals> + <goal>testEnhance</goal> + </goals> + </execution> + + </executions> + <dependencies> + <dependency> + <groupId>io.ebean</groupId> + <artifactId>ebean-agent</artifactId> + <version>${ebean-agent.version}</version> + </dependency> + </dependencies> </plugin> - </plugins> </build> diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java b/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java index e85edb93ef..a27e4b54a5 100644 --- a/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java +++ b/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java @@ -399,6 +399,11 @@ private void initDockerProperties() { } private void setDockerOptionalParameters() { + String mirror = properties.getProperty("ebean.test.containers.mirror"); + if (mirror != null) { + // use a image mirror (when not running locally, i.e. CI) + dockerProperties.setProperty("ebean.test.containers.mirror", mirror); + } // check for shutdown mode on all containers String mode = properties.getProperty("ebean.test.shutdownMode"); if (mode != null) { diff --git a/ebean-test/src/main/java/module-info.java b/ebean-test/src/main/java/module-info.java index 428c3b3f4a..ed64a809eb 100644 --- a/ebean-test/src/main/java/module-info.java +++ b/ebean-test/src/main/java/module-info.java @@ -1,5 +1,5 @@ - -module io.ebean.test { +// module must be open, so tests will pass +open module io.ebean.test { exports io.ebean.test; exports io.ebean.test.config; diff --git a/ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java b/ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java new file mode 100644 index 0000000000..950fbc57dc --- /dev/null +++ b/ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java @@ -0,0 +1,154 @@ +package io.ebean.test.config; + + +import io.ebean.Database; +import io.ebean.DatabaseFactory; +import io.ebean.annotation.Platform; +import io.ebean.config.DatabaseConfig; +import io.ebean.datasource.DataSourceAlert; +import io.ebean.datasource.DataSourceInitialiseException; +import io.ebean.xtest.ForPlatform; + +import io.ebean.xtest.base.PlatformCondition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.tests.model.basic.EBasicVer; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +import javax.persistence.PersistenceException; +import javax.sql.DataSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(PlatformCondition.class) +public class TestServerOffline { + + @Test + @ForPlatform({Platform.H2}) + public void testOffline_default() throws SQLException { + + String url = "jdbc:h2:mem:testoffline1"; + try (Connection bootup = DriverManager.getConnection(url, "sa", "secret")) { + Properties props = props(url); + DatabaseConfig config = config(props); + + assertThatThrownBy(() -> DatabaseFactory.create(config)) + .isInstanceOf(DataSourceInitialiseException.class); + } + + } + + private static class LazyDatasourceInitializer implements DataSourceAlert { + + public Database server; + + private boolean initialized; + + @Override + public void dataSourceUp(DataSource dataSource) { + if (!initialized) { + initDatabase(); + } + } + + public synchronized void initDatabase() { + if (!initialized) { + server.runDdl(); + initialized = true; + } + } + + @Override + public void dataSourceDown(DataSource dataSource, SQLException reason) {} + + @Override + public void dataSourceWarning(DataSource dataSource, String msg) {} + + } + + @Test + @ForPlatform({Platform.H2}) + public void testOffline_recovery() throws SQLException { + + String url = "jdbc:h2:mem:testoffline3"; + try (Connection bootup = DriverManager.getConnection(url, "sa", "secret")) { + + Properties props = props(url); + + // to bring up ebean without a database, we must disable various things + // that happen on startup + props.setProperty("datasource.h2_offline.failOnStart", "false"); + props.setProperty("ebean.h2_offline.skipDataSourceCheck", "true"); + props.setProperty("ebean.h2_offline.ddl.run", "false"); + DatabaseConfig config = config(props); + + LazyDatasourceInitializer alert = new LazyDatasourceInitializer() ; + config.getDataSourceConfig().setAlert(alert); + config.getDataSourceConfig().setHeartbeatFreqSecs(1); + + Database h2Offline = DatabaseFactory.create(config); + alert.server = h2Offline; + assertThat(h2Offline).isNotNull(); + // DB is online now in offline mode + + // Accessing the DB will throw a PE + assertThatThrownBy(() -> alert.initDatabase()) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Failed to obtain connection to run DDL"); + + assertThatThrownBy(() -> h2Offline.find(EBasicVer.class).findCount()).isInstanceOf(PersistenceException.class); + + // so - reset the password so that the server can reconnect + try (Statement stmt = bootup.createStatement()) { + stmt.execute("alter user sa set password 'sa'"); + } + + assertThat(alert.initialized).isFalse(); + + // next access to ebean should bring DS online + h2Offline.find(EBasicVer.class).findCount(); + assertThat(alert.initialized).isTrue(); + + // check if server is working (ie ddl was run) + EBasicVer bean = new EBasicVer("foo"); + h2Offline.save(bean); + assertThat(h2Offline.find(EBasicVer.class).findCount()).isEqualTo(1); + h2Offline.delete(bean); + } + } + + private Properties props(String url) { + + Properties props = new Properties(); + + props.setProperty("datasource.h2_offline.username", "sa"); + props.setProperty("datasource.h2_offline.password", "sa"); + props.setProperty("datasource.h2_offline.url", url); + props.setProperty("datasource.h2_offline.driver", "org.h2.Driver"); + + props.setProperty("ebean.h2_offline.databasePlatformName", "h2"); + props.setProperty("ebean.h2_offline.ddl.extra", "false"); + + props.setProperty("ebean.h2_offline.ddl.generate", "true"); + props.setProperty("ebean.h2_offline.ddl.run", "true"); + + return props; + } + + private DatabaseConfig config(Properties props) { + DatabaseConfig config = new DatabaseConfig(); + config.setName("h2_offline"); + config.loadFromProperties(props); + config.setDefaultServer(false); + config.setRegister(false); + config.getClasses().add(EBasicVer.class); + return config; + } + +} diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java index 101e8a370a..bbe69a235e 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java @@ -1,15 +1,15 @@ package io.ebean.xtest.base; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.DtoQuery; import io.ebean.ProfileLocation; -import io.ebean.xtest.ForPlatform; import io.ebean.annotation.Platform; import io.ebean.meta.MetaQueryMetric; import io.ebean.meta.MetaTimedMetric; import io.ebean.meta.ServerMetrics; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import io.ebean.xtest.ForPlatform; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -329,6 +329,37 @@ public void toDto_fromExpressionList() { assertThat(contactDtos).isNotEmpty(); } + @Test + public void toDto_withQueryCache() { + + ResetBasicData.reset(); + LoggedSql.start(); + List<ContactTotals> contactDtos1 = DB.find(Contact.class) + .setUseQueryCache(true) + .select("lastName, count(*) as totalCount").where() + .isNotNull("lastName").asDto(ContactTotals.class).findList(); + assertThat(LoggedSql.stop()).hasSize(1); + + + LoggedSql.start(); + List<ContactTotals> contactDtos2 = DB.find(Contact.class) + .setUseQueryCache(true) + .select("lastName, count(*) as totalCount").where() + .isNotNull("lastName").asDto(ContactTotals.class).findList(); + assertThat(LoggedSql.stop()).isEmpty(); + assertThat(contactDtos1).isNotEmpty().isSameAs(contactDtos2); + assertThat(contactDtos1.get(0)).isInstanceOf(ContactTotals.class); + + List<ContactTotalsInt> contactDtos3 = DB.find(Contact.class) + .setUseQueryCache(true) + .select("lastName, count(*) as totalCount").where() + .isNotNull("lastName").asDto(ContactTotalsInt.class).findList(); + + assertThat(contactDtos3).isNotEmpty(); + assertThat(contactDtos3.get(0)).isInstanceOf(ContactTotalsInt.class); + + } + public static class ContactTotals { String lastName; @@ -356,6 +387,28 @@ public void setTotalCount(Long totalCount) { } } + public static class ContactTotalsInt { + + String lastName; + int totalCount; + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public int getTotalCount() { + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } + } + public static class ContactDto2 { int id; diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java b/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java index 0dcc4afc28..2915657281 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java +++ b/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java @@ -49,6 +49,39 @@ public void create_new_server_with_multi_tenancy_db() { + /** + * Tests using multi tenancy per database + */ + @Test + public void create_new_server_with_multi_tenancy_db_with_master() { + + String tenant = "customer"; + CurrentTenantProvider tenantProvider = Mockito.mock(CurrentTenantProvider.class); + Mockito.doReturn(tenant).when(tenantProvider).currentId(); + + TenantDataSourceProvider dataSourceProvider = Mockito.mock(TenantDataSourceProvider.class); + + DatabaseConfig config = new DatabaseConfig(); + + config.setName("h2"); + config.loadFromProperties(); + config.setRegister(false); + config.setDefaultServer(false); + config.setDdlGenerate(false); + config.setDdlRun(false); + + config.setTenantMode(TenantMode.DB_WITH_MASTER); + config.setCurrentTenantProvider(tenantProvider); + config.setTenantDataSourceProvider(dataSourceProvider); + + Mockito.doReturn(config.getDataSource()).when(dataSourceProvider).dataSource(tenant); + + config.setDatabasePlatform(new PostgresPlatform()); + + final Database database = DatabaseFactory.create(config); + database.shutdown(); + } + /** * Tests using multi tenancy per schema */ diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/ServerStartTest.java b/ebean-test/src/test/java/io/ebean/xtest/base/ServerStartTest.java new file mode 100644 index 0000000000..b0dfe787b9 --- /dev/null +++ b/ebean-test/src/test/java/io/ebean/xtest/base/ServerStartTest.java @@ -0,0 +1,15 @@ +package io.ebean.xtest.base; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import io.ebean.DatabaseFactory; + +public class ServerStartTest { + + @Test + @Disabled("run manually") + void testServerStartAndMigrateDb2() throws Exception { + DatabaseFactory.create("db2-migration").shutdown(); + } +} diff --git a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java index 4f5471c62c..df9850d57f 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java @@ -79,13 +79,11 @@ public static void main(String[] args) throws IOException { List<String> pendingDrops = migration.getPendingDrops(); assertThat(pendingDrops).contains("1.1"); - //System.setProperty("ddl.migration.pendingDropsFor", "1.1"); migration.setGeneratePendingDrop("1.1"); assertThat(migration.generateMigration()).isEqualTo("1.2__dropsFor_1.1"); assertThatThrownBy(()->migration.generateMigration()) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("No 'pendingDrops'"); // subsequent call - System.clearProperty("ddl.migration.pendingDropsFor"); server.shutdown(); logger.info("end"); diff --git a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java index df70742162..40706eb8b3 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java @@ -4,6 +4,8 @@ import io.ebean.DatabaseFactory; import io.ebean.annotation.Platform; import io.ebean.config.DatabaseConfig; +import io.ebeaninternal.api.DbOffline; + import io.ebean.dbmigration.DbMigration; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -17,7 +19,6 @@ import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** @@ -96,7 +97,7 @@ public static void run(String pathToResources) throws IOException { config.getProperties().put("ebean.hana.generateUniqueDdl", "true"); // need to generate unique statements to prevent them from being filtered out as duplicates by the DdlRunner config.setPackages(Arrays.asList("misc.migration.v1_0")); - Database server = DatabaseFactory.create(config); + Database server = createServer(config); migration.setServer(server); // then we generate migration scripts for v1_0 @@ -107,43 +108,28 @@ public static void run(String pathToResources) throws IOException { // and now for v1_1 config.setPackages(Arrays.asList("misc.migration.v1_1")); server.shutdown(); - server = DatabaseFactory.create(config); + server = createServer(config); migration.setServer(server); - assertThat(migration.generateMigration()).isEqualTo("1.1"); - assertThat(migration.generateMigration()).isNull(); // subsequent call - - - - System.setProperty("ddl.migration.pendingDropsFor", "1.1"); - assertThat(migration.generateMigration()).isEqualTo("1.2__dropsFor_1.1"); - - assertThatThrownBy(()->migration.generateMigration()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("No 'pendingDrops'"); // subsequent call - - System.clearProperty("ddl.migration.pendingDropsFor"); + assertThat(migration.generateMigration()).isEqualTo("1.1,1.2__dropsFor_1.1"); assertThat(migration.generateMigration()).isNull(); // subsequent call // and now for v1_2 with config.setPackages(Arrays.asList("misc.migration.v1_2")); server.shutdown(); - server = DatabaseFactory.create(config); + server = createServer(config); migration.setServer(server); - assertThat(migration.generateMigration()).isEqualTo("1.3"); - assertThat(migration.generateMigration()).isNull(); // subsequent call - - - System.setProperty("ddl.migration.pendingDropsFor", "1.3"); - assertThat(migration.generateMigration()).isEqualTo("1.4__dropsFor_1.3"); - assertThatThrownBy(migration::generateMigration) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("No 'pendingDrops'"); // subsequent call - - System.clearProperty("ddl.migration.pendingDropsFor"); + assertThat(migration.generateMigration()).isEqualTo("1.3,1.4__dropsFor_1.3"); assertThat(migration.generateMigration()).isNull(); // subsequent call server.shutdown(); logger.info("end"); } + private static Database createServer(DatabaseConfig config) { + DbOffline.setGenerateMigration(); + Database server = DatabaseFactory.create(config); + DbOffline.reset(); + return server; + } + } diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java index 124b04a9d5..49a429ed80 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java @@ -110,6 +110,11 @@ public DataTimeZone dataTimeZone() { return null; } + @Override + public int maxStringSize() { + return 0; + } + @Override public Platform platform() { return Platform.GENERIC; @@ -255,6 +260,12 @@ public void merge(Object bean, MergeOptions options) { public void merge(Object bean, MergeOptions options, Transaction transaction) { } + @Override + public <T> T mergeBeans(T bean, T existing, BeanMergeOptions options) { + return null; + } + + @Override public <T> List<Version<T>> findVersions(Query<T> query, Transaction transaction) { return null; @@ -963,7 +974,7 @@ public Set<Property> checkUniqueness(Object bean) { } @Override - public Set<Property> checkUniqueness(Object bean, Transaction transaction) { + public Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) { return Collections.emptySet(); } diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java index 3fa7f84b8a..af7741ab26 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java @@ -404,7 +404,7 @@ public Set<Property> checkUniqueness(Object bean) { } @Override - public Set<Property> checkUniqueness(Object bean, Transaction transaction) { + public Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) { return null; } @@ -448,6 +448,12 @@ public void merge(Object bean, MergeOptions options, Transaction transaction) { } + @Override + public <T> T mergeBeans(T bean, T existing, BeanMergeOptions options) { + return null; + } + + @Override public void insert(Object bean) { @@ -627,4 +633,9 @@ public void loadBeanL2(EntityBeanIntercept ebi) { public void loadBean(EntityBeanIntercept ebi) { } + + @Override + public void runDdl() { + + } } diff --git a/ebean-test/src/test/java/org/multitenant/partition/CurrentTenant.java b/ebean-test/src/test/java/org/multitenant/partition/CurrentTenant.java index 3166f95f2e..4d7d0507b2 100644 --- a/ebean-test/src/test/java/org/multitenant/partition/CurrentTenant.java +++ b/ebean-test/src/test/java/org/multitenant/partition/CurrentTenant.java @@ -1,6 +1,7 @@ package org.multitenant.partition; import io.ebean.config.CurrentTenantProvider; +import io.ebean.test.UserContext; import java.util.concurrent.atomic.AtomicInteger; @@ -18,6 +19,6 @@ static int count() { @Override public String currentId() { callCounter.incrementAndGet(); - return UserContext.get().getTenantId(); + return String.valueOf(UserContext.currentTenantId()); } } diff --git a/ebean-test/src/test/java/org/multitenant/partition/MultiTenantPartitionTest.java b/ebean-test/src/test/java/org/multitenant/partition/MultiTenantPartitionTest.java index 90dc62e444..9c03ae0c27 100644 --- a/ebean-test/src/test/java/org/multitenant/partition/MultiTenantPartitionTest.java +++ b/ebean-test/src/test/java/org/multitenant/partition/MultiTenantPartitionTest.java @@ -4,7 +4,11 @@ import io.ebean.DatabaseFactory; import io.ebean.config.DatabaseConfig; import io.ebean.config.TenantMode; +import io.ebean.meta.MetaQueryPlan; +import io.ebean.meta.QueryPlanInit; +import io.ebean.meta.QueryPlanRequest; import io.ebean.test.LoggedSql; +import io.ebean.test.UserContext; import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; @@ -22,12 +26,13 @@ class MultiTenantPartitionTest extends BaseTestCase { static List<MtTenant> tenants() { List<MtTenant> tenants = new ArrayList<>(); for (int i = 0; i < 5; i++) { - tenants.add(new MtTenant("ten_"+i, names[i], names[i]+"@foo.com".toLowerCase())); + tenants.add(new MtTenant("ten_" + i, names[i], names[i] + "@foo.com".toLowerCase())); } return tenants; } private static final Database server = init(); + static { server.saveAll(tenants()); } @@ -76,6 +81,40 @@ void start() { LoggedSql.stop(); } + @Test + void queryPlanCapture() throws InterruptedException { + // change query plan threshold to 100 micros + + QueryPlanRequest request = new QueryPlanRequest(); + request.maxCount(1_000); + request.maxTimeMillis(10_000); + server.metaInfo().queryPlanCollectNow(request); + + QueryPlanInit init = new QueryPlanInit(); + init.setAll(true); + init.thresholdMicros(1); + server.metaInfo().queryPlanInit(init); + try { + // run queries again + UserContext.set("rob", "ten_1"); + server.find(MtTenant.class).findList(); + + UserContext.set("fred", "ten_2"); + server.find(MtTenant.class, 2); + + // obtains db query plans ... + List<MetaQueryPlan> plans0 = server.metaInfo().queryPlanCollectNow(request); + assertThat(plans0).isNotEmpty(); + + assertThat(plans0).extracting(MetaQueryPlan::tenantId).containsExactlyInAnyOrder("ten_1", "ten_2"); + } finally { + // disable capturing, as it may affect other tests + init.thresholdMicros(Long.MAX_VALUE); + server.metaInfo().queryPlanInit(init); + } + } + + @Test void deleteById() { UserContext.set("fred", "ten_2"); diff --git a/ebean-test/src/test/java/org/multitenant/partition/UserContext.java b/ebean-test/src/test/java/org/multitenant/partition/UserContext.java deleted file mode 100644 index 60794b674f..0000000000 --- a/ebean-test/src/test/java/org/multitenant/partition/UserContext.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.multitenant.partition; - -public final class UserContext { - - private static final UserContextThreadLocal local = new UserContextThreadLocal(); - - private String userId; - private String tenantId; - - private UserContext(String userId, String tenantId) { - this.userId = userId; - this.tenantId = tenantId; - } - - private UserContext() { - } - - public String getUserId() { - return userId; - } - - public String getTenantId() { - return tenantId; - } - - public static UserContext get() { - return local.get(); - } - - public static void reset() { - local.remove(); - } - - public static void set(String userId, String tenantId) { - local.set(new UserContext(userId, tenantId)); - } - - - private static class UserContextThreadLocal extends ThreadLocal<UserContext> { - - @Override - protected UserContext initialValue() { - return new UserContext(); - } - } -} diff --git a/ebean-test/src/test/java/org/tests/basic/TestDeleteByIdCollection.java b/ebean-test/src/test/java/org/tests/basic/TestDeleteByIdCollection.java index 30f2239dee..d2737e89a3 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestDeleteByIdCollection.java +++ b/ebean-test/src/test/java/org/tests/basic/TestDeleteByIdCollection.java @@ -1,6 +1,7 @@ package org.tests.basic; import io.ebean.DB; +import io.ebean.test.LoggedSql; import io.ebean.xtest.base.TransactionalTestCase; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; @@ -10,6 +11,7 @@ import java.util.ArrayList; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -41,8 +43,10 @@ public void test() { DB.deleteAll(Customer.class, ids); awaitL2Cache(); + LoggedSql.start(); c0Back = DB.find(Customer.class, c0.getId()); c1Back = DB.find(Customer.class, "" + c1.getId()); + assertThat(LoggedSql.stop()).isEmpty(); assertNull(c0Back); assertNull(c1Back); diff --git a/ebean-test/src/test/java/org/tests/batchinsert/TestBatchInsertFlush.java b/ebean-test/src/test/java/org/tests/batchinsert/TestBatchInsertFlush.java index 0ba7340926..cae9bd8ae5 100644 --- a/ebean-test/src/test/java/org/tests/batchinsert/TestBatchInsertFlush.java +++ b/ebean-test/src/test/java/org/tests/batchinsert/TestBatchInsertFlush.java @@ -1,23 +1,28 @@ package org.tests.batchinsert; -import io.ebean.*; -import io.ebean.xtest.BaseTestCase; -import io.ebean.xtest.IgnorePlatform; +import io.ebean.DB; +import io.ebean.Database; +import io.ebean.Transaction; import io.ebean.annotation.PersistBatch; import io.ebean.annotation.Platform; import io.ebean.annotation.Transactional; import io.ebean.meta.MetaTimedMetric; import io.ebean.meta.ServerMetrics; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import io.ebean.xtest.IgnorePlatform; import io.ebean.xtest.base.DtoQuery2Test; import io.ebeaninternal.api.SpiTransaction; import org.junit.jupiter.api.Test; +import org.tests.model.basic.Contact; import org.tests.model.basic.Customer; import org.tests.model.basic.EBasicVer; +import org.tests.model.basic.ResetBasicData; import org.tests.model.basic.TSDetail; import org.tests.model.basic.TSMaster; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -25,6 +30,38 @@ public class TestBatchInsertFlush extends BaseTestCase { + @Test + public void batchFlush() { + + TSMaster m = new TSMaster(); + m.setName("master1"); + DB.save(m); + DB.getDefault().cacheManager().clearAll(); + + try (Transaction txn = DB.beginTransaction()) { + txn.setBatchSize(2); + txn.setBatchMode(true); + + List<Customer> customers = new ArrayList<>(); + + for (int i = 0; i < 3; i++) { + Customer customer = ResetBasicData.createCustomer("BatchFlushPreInsert " + i, null, null, 3); + customer.addContact(new Contact("Fred" + i, "Blue")); + customers.add(customer); + } + + for (int i = 3; i < 6; i++) { + Customer customer = ResetBasicData.createCustomer("BatchFlushPostInsert " + i, null, null, 3); + customer.addContact(new Contact("Fred" + i, "Blue")); + customers.add(customer); + } + + DB.saveAll(customers); + + txn.commit(); + } + } + @Test public void no_cascade() { @@ -70,7 +107,7 @@ public void no_cascade() { // detail assertThat(sql.get(3)).contains("insert into t_detail_with_other_namexxxyy"); - assertThat(((SpiTransaction)transaction).getLabel()).isEqualTo("TestBatchInsertFlush.no_cascade"); + assertThat(((SpiTransaction) transaction).getLabel()).isEqualTo("TestBatchInsertFlush.no_cascade"); } finally { transaction.end(); @@ -133,7 +170,7 @@ public void transactional_flushOnDtoQuery() { // trigger JDBC batch by default DB.findDto(DtoQuery2Test.DCust.class, "select id, name from o_customer") - .findList(); + .findList(); List<String> sql = LoggedSql.stop(); assertSql(sql.get(0)).contains("insert into e_basicver"); diff --git a/ebean-test/src/test/java/org/tests/batchload/TestLazyAddBeanList.java b/ebean-test/src/test/java/org/tests/batchload/TestLazyAddBeanList.java new file mode 100644 index 0000000000..961195f7c0 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/batchload/TestLazyAddBeanList.java @@ -0,0 +1,180 @@ +package org.tests.batchload; + +import io.ebean.DB; +import io.ebean.common.BeanList; +import io.ebean.common.BeanListLazyAdd; +import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.tests.model.basic.Contact; +import org.tests.model.basic.Customer; +import org.tests.order.OrderMaster; +import org.tests.order.OrderReferencedChild; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestLazyAddBeanList extends BaseTestCase { + + private Customer cust; + private OrderMaster orderMaster; + + @BeforeEach + void init() { + cust = new Customer(); + cust.setName("noFetch"); + DB.save(cust); + cust = DB.find(Customer.class, cust.getId()); // fetch fresh from DB + + orderMaster = new OrderMaster(); + DB.save(orderMaster); + orderMaster = DB.find(OrderMaster.class, orderMaster.getId()); + } + + @AfterEach + void tearDown() { + DB.delete(cust); + DB.delete(orderMaster); + } + + @Test + public void testCollectionType() { + assertThat(cust.getContacts()) + .isInstanceOf(BeanListLazyAdd.class); + + assertThat(orderMaster.getChildren()) + .isInstanceOf(BeanList.class) + .isNotInstanceOf(BeanListLazyAdd.class); + + assertThat(DB.reference(Customer.class, 1).getContacts()) + .isInstanceOf(BeanListLazyAdd.class); + + assertThat(DB.reference(OrderMaster.class, 1).getChildren()) + .isInstanceOf(BeanList.class) + .isNotInstanceOf(BeanListLazyAdd.class); + } + + @Test + public void testNoFetch() { + + LoggedSql.start(); + cust.addContact(new Contact("jim", "slim")); + cust.addContact(new Contact("joe", "big")); + DB.save(cust); + + List<String> sql = LoggedSql.stop(); + assertThat(sql.get(0)).contains("insert into contact"); + assertThat(sql.get(1)).contains("bind(jim,slim,"); + assertThat(sql.get(2)).contains("bind(joe,big,"); + assertThat(sql).hasSize(3); + + assertThat(cust.getContacts()).hasSize(2); + + List<String> list = DB.find(Customer.class) + .fetch("contacts", "firstName") + .where() + .idEq(cust.getId()).findSingleAttributeList(); +// check if it is really saved + assertThat(list).containsExactlyInAnyOrder("jim", "joe"); + } + + @Test + public void testFetch() { + + LoggedSql.start(); + orderMaster.getChildren().add(new OrderReferencedChild("foo")); + orderMaster.getChildren().add(new OrderReferencedChild("bar")); + DB.save(orderMaster); + + List<String> sql = LoggedSql.stop(); + + assertThat(sql.get(0)).contains("from order_referenced_parent"); // lazy load children + assertThat(sql.get(1)).contains("insert into order_referenced_parent"); + assertThat(sql.get(2)).contains("bind(D,foo,"); + assertThat(sql.get(3)).contains("bind(D,bar,"); + + List<String> list = DB.find(OrderMaster.class) + .fetch("children", "name") + .where().idEq(orderMaster.getId()) + .findSingleAttributeList(); + // check if it is really saved + assertThat(list).containsExactlyInAnyOrder("foo", "bar"); + } + + @Test + public void testAddToExisting() { + + // add some existing entries + LoggedSql.start(); + cust.getContacts().addAll(Arrays.asList( + new Contact("jim", "slim"), + new Contact("joe", "big"))); + assertThat(LoggedSql.stop()).isEmpty(); + + LoggedSql.start(); + DB.save(cust); + assertThat(LoggedSql.stop()).hasSize(3); // insert + 2x bind + + cust = DB.find(Customer.class, cust.getId()); + + LoggedSql.start(); + List<Contact> contacts = cust.getContacts(); + contacts.add(new Contact("charlie", "brown")); + assertThat(LoggedSql.stop()).isEmpty(); + + LoggedSql.start(); + assertThat(contacts). + extracting(Contact::getFirstName). + containsExactlyInAnyOrder("jim", "joe", "charlie"); + List<String> sql = LoggedSql.stop(); + + assertThat(sql.get(0)).contains("from o_customer t0 left join contact t1 "); + assertThat(sql).hasSize(1); + } + + @Test + public void testBatch() { + for (int i = 0; i < 10; i++) { + Customer bcust = new Customer(); + bcust.setName("batch " + i); + //bcust.getContacts().add(new Contact("Noemi","Praml")); + DB.save(bcust); + } + + LoggedSql.start(); + List<Customer> custs = DB.find(Customer.class).where().startsWith("name", "batch").findList(); + assertThat(custs).hasSize(10); + assertThat(LoggedSql.stop()).hasSize(1); + + + LoggedSql.start(); + for (Customer cust : custs) { + cust.getContacts().addAll(Arrays.asList( + new Contact(cust.getName() + " jim", "slim"), + new Contact(cust.getName() + " joe", "big"))); + } + assertThat(LoggedSql.stop()).isEmpty(); + + LoggedSql.start(); + custs.get(0).getContacts().size(); // trigger batch load + assertThat(LoggedSql.stop()).hasSize(1); + + LoggedSql.start(); + DB.saveAll(custs); + assertThat(LoggedSql.stop()).hasSize(21); + + LoggedSql.start(); + custs = DB.find(Customer.class).where().startsWith("name", "batch").findList(); + assertThat(custs).hasSize(10); + + for (Customer cust : custs) { + assertThat(cust.getContacts()).hasSize(2).extracting(Contact::getFirstName).containsExactlyInAnyOrder(cust.getName() + " jim", cust.getName() + " joe"); + } + assertThat(LoggedSql.stop()).hasSize(2); + } + +} diff --git a/ebean-test/src/test/java/org/tests/cache/TestBeanCache.java b/ebean-test/src/test/java/org/tests/cache/TestBeanCache.java index 2028a05071..d43fe65366 100644 --- a/ebean-test/src/test/java/org/tests/cache/TestBeanCache.java +++ b/ebean-test/src/test/java/org/tests/cache/TestBeanCache.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -67,7 +68,7 @@ public void idsInFindMap() { // Test findIds LoggedSql.start(); - query.copy() + Map<Object, OCachedBean> map1 = query.copy() .where().idIn(ids.subList(0, 1)) .findMap(); // cache key is: 3/d[{/c1000}]/w[List[IdIn[?1],]] if (isPostgresCompatible()) { @@ -77,7 +78,7 @@ public void idsInFindMap() { } LoggedSql.start(); - query.copy() + Map<Object, OCachedBean> map2 = query.copy() .where().idIn(ids.subList(0, 4)) .findMap(); // cache key is: 3/d[{/c1000}]/w[List[IdIn[?5],]] if (isPostgresCompatible()) { @@ -87,7 +88,7 @@ public void idsInFindMap() { } LoggedSql.start(); - query.copy() + Map<Object, OCachedBean> map3 = query.copy() .where().idIn(ids.subList(2, 6)) .findMap(); // same cache key as above and same SQL above if (isPostgresCompatible()) { @@ -95,6 +96,112 @@ public void idsInFindMap() { } else { assertThat(LoggedSql.stop().get(0)).contains("in (?,?,?,?,?)"); } + + ServerCache bc = DB.getDefault().pluginApi().cacheManager().beanCache(OCachedBean.class); + bc.statistics(true); + + ServerCache qc = DB.getDefault().pluginApi().cacheManager().queryCache(OCachedBean.class); + qc.statistics(true); + + LoggedSql.start(); + assertThat(query.copy() + .where().idIn(ids.subList(0, 1)) + .findMap()).isEqualTo(map1).isNotSameAs(map1); + + + assertThat(query.copy() + .where().idIn(ids.subList(0, 4)) + .findMap()).isEqualTo(map2).isNotSameAs(map2); + + assertThat(query.copy() + .where().idIn(ids.subList(2, 6)) + .findMap()).isEqualTo(map3).isNotSameAs(map3); + + assertThat(LoggedSql.stop()).isEmpty(); // we should have no DB-hits + + // we should have all beans in the bean cache, but no hits in the query cache + assertThat(bc.statistics(true).getHitCount()).isEqualTo(map1.size() + map2.size() + map3.size()); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(0); + + // check with a different set, that should be in the cache + query.copy().where().idIn(ids.subList(2, 5)).findMap(); + assertThat(bc.statistics(true).getHitCount()).isEqualTo(3); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(0); + + } + + + @Test + public void idsInFindMapWithBeanAndQueryCache() { + + List<OCachedBean> beans = createBeans(Arrays.asList("m0", "m1", "m2", "m3", "m4", "m5", "m6")); + List<Long> ids = beans.stream().map(OCachedBean::getId).collect(Collectors.toList()); + beanCache.clear(); + beanCache.statistics(true); + Query<OCachedBean> query = DB.find(OCachedBean.class) + .setUseCache(true) + .setUseQueryCache(true) + .setReadOnly(true); + + // Test findIds + LoggedSql.start(); + Map<Object, OCachedBean> map1 = query.copy() + .where().idIn(ids.subList(0, 1)) + .findMap(); // cache key is: 3/d[{/c1000}]/w[List[IdIn[?1],]] + if (isPostgresCompatible()) { + assertThat(LoggedSql.stop().get(0)).contains("t0.id = any(?)"); + } else { + assertThat(LoggedSql.stop().get(0)).contains("in (?)"); + } + + LoggedSql.start(); + Map<Object, OCachedBean> map2 = query.copy() + .where().idIn(ids.subList(0, 4)) + .findMap(); // cache key is: 3/d[{/c1000}]/w[List[IdIn[?5],]] + if (isPostgresCompatible()) { + assertThat(LoggedSql.stop().get(0)).contains("t0.id = any(?)"); + } else { + assertThat(LoggedSql.stop().get(0)).contains("in (?,?,?,?,?)"); + } + + LoggedSql.start(); + Map<Object, OCachedBean> map3 = query.copy() + .where().idIn(ids.subList(2, 6)) + .findMap(); // same cache key as above and same SQL above + if (isPostgresCompatible()) { + assertThat(LoggedSql.stop().get(0)).contains("t0.id = any(?)"); + } else { + assertThat(LoggedSql.stop().get(0)).contains("in (?,?,?,?,?)"); + } + + ServerCache bc = DB.getDefault().pluginApi().cacheManager().beanCache(OCachedBean.class); + bc.statistics(true); + + ServerCache qc = DB.getDefault().pluginApi().cacheManager().queryCache(OCachedBean.class); + qc.statistics(true); + + LoggedSql.start(); + assertThat(query.copy() + .where().idIn(ids.subList(0, 1)) + .findMap()).isEqualTo(map1).isSameAs(map1); + + assertThat(query.copy() + .where().idIn(ids.subList(0, 4)) + .findMap()).isEqualTo(map2).isSameAs(map2); + + assertThat(query.copy() + .where().idIn(ids.subList(2, 6)) + .findMap()).isEqualTo(map3).isSameAs(map3); + + assertThat(LoggedSql.stop()).isEmpty(); // we should have no DB-hits + + assertThat(bc.statistics(true).getHitCount()).isEqualTo(0); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(3); + + // check with a different set, that should be in the cache + query.copy().where().idIn(ids.subList(2, 5)).findMap(); + assertThat(bc.statistics(true).getHitCount()).isEqualTo(3); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(0); } @Test diff --git a/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java b/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java index 6970dce776..ed68ef88bb 100644 --- a/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java +++ b/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java @@ -3,12 +3,14 @@ import io.ebean.CacheMode; import io.ebean.DB; import io.ebean.ExpressionList; +import io.ebean.Query; import io.ebean.annotation.Transactional; import io.ebean.annotation.TxIsolation; import io.ebean.bean.BeanCollection; import io.ebean.cache.ServerCache; import io.ebean.test.LoggedSql; import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.ResetBasicData; @@ -302,6 +304,92 @@ public void findCountFirstRecacheThenOn() { } + /** + * This test primarily checks, if the query cache on findById will work properly. + * + * It also checks some special cases, if query + bean cache are combined with other queries. + * It is important to know, that a "findId" query, that is not yet compiled will use the bean cache. + */ + @Test + @SuppressWarnings("unchecked") + public void testFindByIdWihtBothCaches() { + + ResetBasicData.reset(); + + List<Object> ids = DB.find(Customer.class).setMaxRows(5).findIds(); + Customer c = DB.find(Customer.class).setMaxRows(1).findOne(); + + DB.getDefault().pluginApi().cacheManager().clearAll(); + + ServerCache bc = DB.getDefault().pluginApi().cacheManager().beanCache(Customer.class); + ServerCache qc = DB.getDefault().pluginApi().cacheManager().queryCache(Customer.class); + bc.statistics(true); + qc.statistics(true); + + // 1. load the bean cache with some beans + DB.find(Customer.class).where().idIn(ids).findList(); + + Query<Customer> q = DB.find(Customer.class).setUseQueryCache(true).setUseCache(true).setReadOnly(true); + LoggedSql.start(); + Customer c1 = q.copy().where().eq("name", c.getName()).findOne(); + Customer c2 = q.copy().where().eq("name", c.getName()).findOne(); + assertThat(LoggedSql.stop()).hasSize(1); + + assertTrue(DB.beanState(c1).isReadOnly()); + assertTrue(DB.beanState(c2).isReadOnly()); + assertThat(c1).isSameAs(c2); + assertThat(bc.statistics(true).getHitCount()).isEqualTo(0); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(1); + + + LoggedSql.start(); + c1 = q.copy().where().eq("id", c.getId()).findOne(); + c2 = q.copy().where().eq("id", c.getId()).findOne(); + assertThat(LoggedSql.stop()).isEmpty(); + assertThat(c1).isNotSameAs(c2); + + LoggedSql.start(); + c1 = q.copy().setId(c.getId()).findOne(); + c2 = q.copy().setId(c.getId()).findOne(); + assertThat(LoggedSql.stop()).isEmpty(); + assertThat(c1).isNotSameAs(c2); + + LoggedSql.start(); + List<Customer> l1 = q.copy().where().idIn(ids.subList(0,2)).findList(); + List<Customer> l2 = q.copy().where().idIn(ids.subList(0,2)).findList(); + assertThat(LoggedSql.stop()).isEmpty(); + assertThat(l1).hasSize(2).isNotSameAs(l2); + + assertThat(bc.statistics(true).getHitCount()).isEqualTo(8); // 4x findOne and 2x findList with 2 elements + assertThat(qc.statistics(true).getHitCount()).isEqualTo(0); + // Note: The ID queries are immediately handled by the BeanCache, because the underlying queries are never compiled + // and so they have no query plan, which is required for cache access. + // + // So we clear the cache and try it again + DB.getDefault().pluginApi().cacheManager().clearAll(); + + LoggedSql.start(); + c1 = q.copy().where().eq("id", c.getId()).findOne(); + c2 = q.copy().where().eq("id", c.getId()).findOne(); + assertThat(LoggedSql.stop()).hasSize(1); + assertThat(c1).isSameAs(c2); + + LoggedSql.start(); + c1 = q.copy().setId(c.getId()).findOne(); + c2 = q.copy().setId(c.getId()).findOne(); + assertThat(LoggedSql.stop()).isEmpty(); // setId(..) query has same queryPlan as eq("id", ..) + assertThat(c1).isSameAs(c2); + + LoggedSql.start(); + l1 = q.copy().where().idIn(1, 2).findList(); + l2 = q.copy().where().idIn(1, 2).findList(); + assertThat(LoggedSql.stop()).hasSize(1); // we have to hit DB for bean#2 + assertThat(l1).hasSize(2).isSameAs(l2); + + assertThat(bc.statistics(true).getHitCount()).isEqualTo(1); // findList can get one bean from bc + assertThat(qc.statistics(true).getHitCount()).isEqualTo(4); // 6 queries, 2 of them hit db + } + @Test @SuppressWarnings("unchecked") public void testReadOnlyFind() { diff --git a/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java b/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java index ced209f8bc..ee97cc1467 100644 --- a/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java +++ b/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java @@ -1,10 +1,13 @@ package org.tests.generated; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; +import io.ebean.Transaction; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; import org.tests.model.EGenProps; +import java.time.Instant; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -47,4 +50,42 @@ public void test_insert() { DB.delete(bean); } + + @Test + public void test_update_no_overwrite() { + EGenProps bean = new EGenProps(); + bean.setName("updating"); + DB.save(bean); + + bean = DB.find(EGenProps.class, bean.getId()); + bean.setInstantCreated(Instant.parse("2022-01-01T00:00:00Z")); + bean.setInstantUpdated(Instant.parse("2022-01-02T00:00:00Z")); + try (Transaction txn = DB.beginTransaction()) { + txn.setOverwriteGeneratedProperties(false); + DB.save(bean); + txn.commit(); + } + + bean = DB.find(EGenProps.class, bean.getId()); + assertThat(bean.getInstantCreated()).isEqualTo(Instant.parse("2022-01-01T00:00:00Z")); + assertThat(bean.getInstantUpdated()).isEqualTo(Instant.parse("2022-01-02T00:00:00Z")); + } + + @Test + public void test_insert_no_overwrite() { + EGenProps bean = new EGenProps(); + try (Transaction txn = DB.beginTransaction()) { + txn.setOverwriteGeneratedProperties(false); + bean.setName("inserting"); + bean.setInstantCreated(Instant.parse("2022-01-01T00:00:00Z")); + bean.setInstantUpdated(Instant.parse("2022-01-02T00:00:00Z")); + DB.save(bean); + txn.commit(); + } + + + bean = DB.find(EGenProps.class, bean.getId()); + assertThat(bean.getInstantCreated()).isEqualTo(Instant.parse("2022-01-01T00:00:00Z")); + assertThat(bean.getInstantUpdated()).isEqualTo(Instant.parse("2022-01-02T00:00:00Z")); + } } diff --git a/ebean-test/src/test/java/org/tests/insert/TestInsertCheckUnique.java b/ebean-test/src/test/java/org/tests/insert/TestInsertCheckUnique.java index 176748edc8..0efa0272ee 100644 --- a/ebean-test/src/test/java/org/tests/insert/TestInsertCheckUnique.java +++ b/ebean-test/src/test/java/org/tests/insert/TestInsertCheckUnique.java @@ -1,13 +1,16 @@ package org.tests.insert; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Transaction; import io.ebean.plugin.Property; +import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.tests.model.basic.EBasicWithUniqueCon; import org.tests.model.draftable.Document; +import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -94,19 +97,131 @@ public void example() { StringBuilder msg = new StringBuilder(); - properties.forEach((it)-> { + properties.forEach((it) -> { Object propertyValue = it.value(doc2); String propertyName = it.name(); - msg.append(" property["+propertyName+"] value["+propertyValue+"]"); + msg.append(" property[" + propertyName + "] value[" + propertyValue + "]"); }); - System.out.println("uniqueProperties > "+uniqueProperties); - System.out.println(" custom msg > " + msg.toString()); + System.out.println("uniqueProperties > " + uniqueProperties); + System.out.println(" custom msg > " + msg); } - + LoggedSql.start(); assertThat(DB.checkUniqueness(doc2).toString()).contains("title"); + List<String> sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id from document t0 where t0.title = ?"); + + + } + } + + /** + * When invoking checkUniqueness multiple times, we can benefit from the "exists" query cache if bean has query cache enabled + */ + @Test + public void testUseQueryCache() { + DB.find(EBasicWithUniqueCon.class).delete(); // clean up DB (otherwise test may be affected by other test) + + EBasicWithUniqueCon basic = new EBasicWithUniqueCon(); + basic.setName("foo"); + basic.setOther("bar"); + basic.setOtherOne("baz"); + + // create a new bean + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, true, false)).isEmpty(); + List<String> sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select t0.id from e_basicverucon t0 where t0.name = ?"); + assertThat(sql.get(1)).contains("select t0.id from e_basicverucon t0 where t0.other = ? and t0.other_one = ?"); + DB.save(basic); + try { + // reload from database + basic = DB.find(EBasicWithUniqueCon.class, basic.getId()); + + // and check again + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, true, false)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select t0.id from e_basicverucon t0 where t0.id <> ? and t0.name = ?"); + assertThat(sql.get(1)).contains("select t0.id from e_basicverucon t0 where t0.id <> ? and t0.other = ? and t0.other_one = ?"); + + // and check again - expect to hit query cache + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, true, false)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).as("Expected to hit query cache").hasSize(0); + + // and check again, where only one value is changed + basic.setOther("fooo"); + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, true, false)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("fooo,baz)"); + + } finally { + DB.delete(EBasicWithUniqueCon.class, basic.getId()); + } + } + + + /** + * When invoking checkUniqueness multiple times, we can benefit from the "exists" query cache if bean has query cache enabled + */ + @Test + public void testSkipClean() { + DB.find(EBasicWithUniqueCon.class).delete(); // clean up DB (otherwise test may be affected by other test) + + EBasicWithUniqueCon basic = new EBasicWithUniqueCon(); + basic.setName("foo"); + basic.setOther("bar"); + basic.setOtherOne("baz"); + + // create a new bean + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, false, true)).isEmpty(); + List<String> sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select t0.id from e_basicverucon t0 where t0.name = ?"); + assertThat(sql.get(1)).contains("select t0.id from e_basicverucon t0 where t0.other = ? and t0.other_one = ?"); + DB.save(basic); + try (Transaction txn = DB.beginTransaction()) { + // reload from database + basic = DB.find(EBasicWithUniqueCon.class, basic.getId()); + + // and check again. We do not check unmodified properties + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, txn, false, true)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(0); + + // and check again, where only one value is changed + basic.setOther("fooo"); + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, txn, false, true)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("fooo,baz)"); + + // multiple checks will hit DB + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, txn, false, true)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + + // enable also query cache + assertThat(DB.checkUniqueness(basic, txn, true, true)).isEmpty(); + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, txn, true, true)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).isEmpty(); + } finally { + DB.delete(EBasicWithUniqueCon.class, basic.getId()); } } } diff --git a/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java b/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java index 47fbfdc582..4826e3873e 100644 --- a/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java +++ b/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java @@ -1,15 +1,25 @@ package org.tests.json; import com.fasterxml.jackson.databind.ObjectMapper; -import io.ebean.xtest.BaseTestCase; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.ebean.DB; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; +import org.tests.model.json.BasicJacksonType; import org.tests.model.json.EBasicJsonJackson; import org.tests.model.json.EBasicJsonJackson2; import org.tests.model.json.LongJacksonType; import org.tests.model.json.StringJacksonType; import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -59,9 +69,56 @@ public void testJsonDeserializeAnnotation() throws IOException { assertThat(found.getValueMap()).containsEntry(1L, "one").containsEntry(2L, "two"); } + public static class DtoJackson { + @DbJson(length = 700) + Set<BasicJacksonType<?>> valueSet = new LinkedHashSet<>(); + + @DbJsonB + List<BasicJacksonType<?>> valueList = new ArrayList<>(); + + @DbJson(length = 700) + @JsonDeserialize(keyAs = Long.class) + Map<Number, BasicJacksonType<?>> valueMap = new LinkedHashMap<>(); + + @DbJson(length = 500) + BasicJacksonType<?> plainValue; + + public Set<BasicJacksonType<?>> getValueSet() { + return valueSet; + } + + public void setValueSet(Set<BasicJacksonType<?>> valueSet) { + this.valueSet = valueSet; + } + + public List<BasicJacksonType<?>> getValueList() { + return valueList; + } + + public void setValueList(List<BasicJacksonType<?>> valueList) { + this.valueList = valueList; + } + + public Map<Number, BasicJacksonType<?>> getValueMap() { + return valueMap; + } + + public void setValueMap(Map<Number, BasicJacksonType<?>> valueMap) { + this.valueMap = valueMap; + } + + public BasicJacksonType<?> getPlainValue() { + return plainValue; + } + + public void setPlainValue(BasicJacksonType<?> plainValue) { + this.plainValue = plainValue; + } + } + /** * This testcase verifies if polymorph objects will work in ebean. - * + * <p> * for BasicJacksonType there exists two types and has a @JsonTypeInfo * annotation. It is expected that this information is also honored by ebean. */ @@ -94,7 +151,8 @@ public void testPolymorph() throws IOException { assertThat(found.getPlainValue()).isInstanceOf(LongJacksonType.class); assertThat(found.getValueList()).hasSize(2); assertThat(found.getValueSet()).hasSize(2); - assertThat(found.getValueMap()).hasSize(2);; + assertThat(found.getValueMap()).hasSize(2); + ; DB.save(bean); @@ -103,7 +161,16 @@ public void testPolymorph() throws IOException { assertThat(found.getPlainValue()).isInstanceOf(LongJacksonType.class); assertThat(found.getValueList()).hasSize(2); assertThat(found.getValueSet()).hasSize(2); - assertThat(found.getValueMap()).hasSize(2);; + assertThat(found.getValueMap()).hasSize(2); + + + DtoJackson dto = DB.find(EBasicJsonJackson2.class).setId(bean.getId()) + .select("valueSet,valueList,valueMap,plainValue").asDto(DtoJackson.class).findOne(); + + assertThat(dto.getPlainValue()).isInstanceOf(LongJacksonType.class); + assertThat(dto.getValueList()).hasSize(2); + assertThat(dto.getValueSet()).hasSize(2); + assertThat(dto.getValueMap()).hasSize(2); } diff --git a/ebean-test/src/test/java/org/tests/json/TestDbJson_List.java b/ebean-test/src/test/java/org/tests/json/TestDbJson_List.java index 27434601ef..0e6c7d60fb 100644 --- a/ebean-test/src/test/java/org/tests/json/TestDbJson_List.java +++ b/ebean-test/src/test/java/org/tests/json/TestDbJson_List.java @@ -2,19 +2,33 @@ import io.ebean.xtest.BaseTestCase; import io.ebean.DB; +import io.ebean.Database; +import io.ebean.DatabaseFactory; +import io.ebean.ValuePair; import io.ebean.xtest.ForPlatform; +import io.ebean.annotation.MutationDetection; import io.ebean.annotation.Platform; +import io.ebean.config.DatabaseConfig; import io.ebean.test.LoggedSql; import io.ebean.text.TextException; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.tests.model.json.EBasicJsonList; import org.tests.model.json.PlainBean; import javax.persistence.PersistenceException; -import java.util.*; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class TestDbJson_List extends BaseTestCase { @@ -231,4 +245,43 @@ public void testNullToEmpty() { assertThat(bean.getTags()).isEmpty(); assertThat(bean.getBeanMap()).isEmpty(); } + + @Test + @ForPlatform(Platform.H2) + @Disabled("breaks everything") + public void testDirtyValues() { + DatabaseConfig config = new DatabaseConfig(); + config.loadFromProperties(); + config.setDefaultServer(true); + config.setRegister(true); + config.setDdlRun(false); + config.setJsonMutationDetection(MutationDetection.SOURCE); + Database db = DatabaseFactory.create(config); + try { + assertThat(db).isNotNull(); + + EBasicJsonList bean = new EBasicJsonList(); + bean.getTags().add("aa"); + bean.getTags().add("bb"); + + db.save(bean); + bean = db.find(EBasicJsonList.class, bean.getId()); + + bean.getTags().add("cc"); + final Map<String, ValuePair> dirtyValues = db.beanState(bean).dirtyValues(); + assertThat(dirtyValues).containsOnlyKeys("tags"); + + final ValuePair diff = dirtyValues.get("tags"); + assertThat(diff.getOldValue()).isInstanceOf(List.class).asList() + .containsExactly("aa", "bb"); + assertThat(diff.getNewValue()).isInstanceOf(List.class).asList() + .containsExactly("aa", "bb", "cc"); + } finally { + if (db != null) { + db.shutdown(); + } + } + + + } } diff --git a/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java b/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java index 8f36be0cbc..280b4221cb 100644 --- a/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java +++ b/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java @@ -1,6 +1,7 @@ package org.tests.lazyforeignkeys; import io.ebean.annotation.DbForeignKey; +import io.ebean.annotation.NotNull; import org.tests.model.basic.Cat; import javax.persistence.*; @@ -29,6 +30,12 @@ public class MainEntityRelation { @DbForeignKey(noConstraint = true) private Cat cat; + @ManyToOne + @NotNull + @JoinColumn(name = "cat2_id") + @DbForeignKey(noConstraint = true) + private Cat cat2; + private String attr1; public MainEntity getEntity1() { @@ -55,6 +62,14 @@ public void setCat(Cat cat) { this.cat = cat; } + public Cat getCat2() { + return cat2; + } + + public void setCat2(Cat cat2) { + this.cat2 = cat2; + } + public String getAttr1() { return attr1; } diff --git a/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java b/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java index f88f148297..771862a953 100644 --- a/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java +++ b/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java @@ -10,9 +10,11 @@ import org.junit.jupiter.api.Test; import org.tests.model.basic.Cat; +import javax.persistence.EntityNotFoundException; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; public class TestLazyForeignKeys extends BaseTestCase { @@ -32,6 +34,10 @@ public void prepare() { rel1.setEntity1(e1); rel1.setEntity2(e2); + + Cat cat = new Cat(); + cat.setId(4711L); + rel1.setCat2(cat); DB.save(rel1); } @@ -57,13 +63,25 @@ public void testFindOne() throws Exception { List<String> sql = LoggedSql.stop(); assertThat(sql).hasSize(3); - assertSql(sql.get(0)).contains("select t0.id, t0.attr1, t0.id1, t0.id2, t1.species, t0.cat_id from main_entity_relation t0 left join animal t1 on t1.id = t0.cat_id"); + assertSql(sql.get(0)).contains("select t0.id, t0.attr1, t0.id1, t0.id2, t1.species, t0.cat_id, t2.species, t0.cat2_id " + + "from main_entity_relation t0 left join animal t1 on t1.id = t0.cat_id left join animal t2 on t2.id = t0.cat2_id"); if (isSqlServer() || isOracle()) { assertSql(sql.get(1)).contains("select t0.id, t0.attr1, t0.attr2, CASE WHEN t0.id is null THEN 1 ELSE 0 END from main_entity t0"); } else { assertSql(sql.get(1)).contains("select t0.id, t0.attr1, t0.attr2, t0.id is null from main_entity t0"); assertSql(sql.get(2)).contains("select t0.id, t0.attr1, t0.attr2, t0.id is null from main_entity t0"); } + + assertThat(rel1.getCat2().getId()).isEqualTo(4711L); + assertThatThrownBy(() -> rel1.getCat2().getName()).isInstanceOf(EntityNotFoundException.class); + + Cat cat = new Cat(); + cat.setId(4711L); + cat.setName("miau"); + DB.save(cat); + + DB.refresh(rel1); + assertThat(rel1.getCat2().getName()).isEqualTo("miau"); } @Test diff --git a/ebean-test/src/test/java/org/tests/model/basic/Contact.java b/ebean-test/src/test/java/org/tests/model/basic/Contact.java index a4d6700714..2d2f639c8d 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/Contact.java +++ b/ebean-test/src/test/java/org/tests/model/basic/Contact.java @@ -12,7 +12,7 @@ @Index(columnNames = {"last_name", "first_name"}) @ChangeLog @Entity -@Cache(naturalKey = "email") +@Cache(naturalKey = "email", enableQueryCache = true) public class Contact { @Id @GeneratedValue diff --git a/ebean-test/src/test/java/org/tests/model/basic/Customer.java b/ebean-test/src/test/java/org/tests/model/basic/Customer.java index 0b4f98539c..48b2c62910 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/Customer.java +++ b/ebean-test/src/test/java/org/tests/model/basic/Customer.java @@ -90,7 +90,7 @@ public String getValue() { List<Order> orders; @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) - List<Contact> contacts; + List<Contact> contacts = new ArrayList<>(); @Override public String toString() { diff --git a/ebean-test/src/test/java/org/tests/model/basic/EBasicWithUniqueCon.java b/ebean-test/src/test/java/org/tests/model/basic/EBasicWithUniqueCon.java index 20a1326b2b..6573b3007e 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/EBasicWithUniqueCon.java +++ b/ebean-test/src/test/java/org/tests/model/basic/EBasicWithUniqueCon.java @@ -1,10 +1,13 @@ package org.tests.model.basic; +import io.ebean.annotation.Cache; + import javax.persistence.*; import javax.validation.constraints.Size; import java.sql.Timestamp; @Entity +@Cache(enableQueryCache = true) @Table(name = "e_basicverucon") @UniqueConstraint(columnNames = {"other", "other_one"}) public class EBasicWithUniqueCon { diff --git a/ebean-test/src/test/java/org/tests/model/basic/MDateTime.java b/ebean-test/src/test/java/org/tests/model/basic/MDateTime.java new file mode 100644 index 0000000000..010d378603 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/MDateTime.java @@ -0,0 +1,88 @@ +package org.tests.model.basic; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZonedDateTime; +import java.util.Calendar; + +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +public class MDateTime { + + @Id + private Integer id; + + @Nullable + private LocalTime localTime; + + @Nullable + private LocalDateTime localDateTime; + + @Nullable + private LocalDate localDate; + + @Nullable + private OffsetDateTime offsetDateTime; + + @Nullable + private ZonedDateTime zonedDateTime; + + @Nullable + private YearMonth propYearMonth; + + @Nullable + private MonthDay propMonthDay; + + @Nullable + private Year propYear; + + @Nullable + private Instant propInstant; + + @Nullable + private Calendar propCalendar; + + @Nullable + private Timestamp propTimestamp; + + @Nullable + private java.sql.Date sqlDate; + + @Nullable + private java.sql.Time sqlTime; + + @Nullable + private java.util.Date utilDate; + + @Nullable + private org.joda.time.DateTime jodaDateTime; + + @Nullable + private org.joda.time.LocalDateTime jodaLocalDateTime; + + @Nullable + private org.joda.time.LocalDate jodaLocalDate; + + @Nullable + private org.joda.time.LocalTime jodaLocalTime; + + @Nullable + private org.joda.time.DateMidnight jodaDateMidnight; + + public Integer getId() { + return id; + } + public void setId(Integer id) { + this.id = id; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/basic/OCachedBean.java b/ebean-test/src/test/java/org/tests/model/basic/OCachedBean.java index abe945bb51..1919822cb9 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/OCachedBean.java +++ b/ebean-test/src/test/java/org/tests/model/basic/OCachedBean.java @@ -9,7 +9,7 @@ /** * Cached bean for testing caching implementation. */ -@Cache +@Cache(enableQueryCache = true) @Entity @Table(name = "o_cached_bean") public class OCachedBean { diff --git a/ebean-test/src/test/java/org/tests/model/basic/TreeNode.java b/ebean-test/src/test/java/org/tests/model/basic/TreeNode.java new file mode 100644 index 0000000000..674d0f8241 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/TreeNode.java @@ -0,0 +1,31 @@ +package org.tests.model.basic; + +import io.ebean.annotation.Formula; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import java.util.List; + +/** + * @author Roland Praml, FOCONIS AG + */ +@Entity +public class TreeNode { + + @Id + private int id; + + @ManyToOne + private TreeNode parent; + @OneToMany + private List<TreeNode> children; + + private int softRef; + + @Formula(select = "${ta}_ref.id", join = "left join e_basic ${ta}_ref on ${ta}_ref.id = ${ta}.soft_ref") + @ManyToOne + private EBasic ref; + +} diff --git a/ebean-test/src/test/java/org/tests/model/basic/event/CustomerPersistAdapter.java b/ebean-test/src/test/java/org/tests/model/basic/event/CustomerPersistAdapter.java index 9ff0b4acfa..197f6e2415 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/event/CustomerPersistAdapter.java +++ b/ebean-test/src/test/java/org/tests/model/basic/event/CustomerPersistAdapter.java @@ -1,8 +1,10 @@ package org.tests.model.basic.event; +import io.ebean.DB; import io.ebean.event.BeanPersistAdapter; import io.ebean.event.BeanPersistRequest; import org.tests.model.basic.Customer; +import org.tests.model.basic.TSMaster; public class CustomerPersistAdapter extends BeanPersistAdapter { @@ -16,7 +18,9 @@ public boolean preInsert(BeanPersistRequest<?> request) { // StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); // request.getTransaction().log("+++++ "+Arrays.toString(stackTrace)); - + if (((Customer) request.bean()).getName().startsWith("BatchFlushPreInsert")) { + DB.find(TSMaster.class).where().eq("name", "master1").exists(); + } return true; } @@ -28,4 +32,11 @@ public boolean preUpdate(BeanPersistRequest<?> request) { return true; } + @Override + public void postInsert(BeanPersistRequest<?> request) { + super.postInsert(request); + if (((Customer) request.bean()).getName().startsWith("BatchFlushPostInsert")) { + DB.find(TSMaster.class).where().eq("name", "master1").exists(); + } + } } diff --git a/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java b/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java index 28e7636a49..c78b3a2cc7 100644 --- a/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java +++ b/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java @@ -20,6 +20,20 @@ public class MnyEdge { @ManyToOne private MnyNode to; + public MnyEdge() { + } + + public MnyEdge(Object from, Object to) { + this.from = (MnyNode) from; + this.to = (MnyNode) to; + this.id = this.from.id * 10000 + this.to.id; + this.flags = this.from.id + this.to.id; + } + + public static MnyEdge createReverseRelation(Object to, MnyNode from) { + return new MnyEdge(from, to); + } + private int flags; public Integer getId() { diff --git a/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java b/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java index fabb580403..71444320fe 100644 --- a/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java +++ b/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java @@ -3,6 +3,7 @@ import io.ebean.annotation.Identity; import io.ebean.annotation.Platform; import io.ebean.annotation.Where; +import io.ebean.annotation.ext.IntersectionFactory; import javax.persistence.*; import java.util.List; @@ -16,16 +17,19 @@ public class MnyNode { String name; - @ManyToMany + @ManyToMany(cascade = CascadeType.ALL) @JoinTable(name = "mny_edge", joinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id")) + @IntersectionFactory(MnyEdge.class) + @Where(clause = "${mta}.flags != 12345 and '${dbTableName}' = 'mny_node'") List<MnyNode> allRelations; - @ManyToMany + @ManyToMany(cascade = CascadeType.ALL) @JoinTable(name = "mny_edge", joinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id")) + @IntersectionFactory(value =MnyEdge.class, factoryMethod = "createReverseRelation") List<MnyNode> allReverseRelations; @ManyToMany diff --git a/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java b/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java index d9ac37ffcc..2557d35e72 100644 --- a/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java +++ b/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java @@ -1,8 +1,8 @@ package org.tests.model.m2m; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -14,10 +14,78 @@ * Tests M2M with complex where queries. * * @author Roland Praml, FOCONIS AG - * */ public class TestM2MWithWhere extends BaseTestCase { + @Test + public void testModify() throws Exception { + + MnyNode node1 = new MnyNode(); + node1.setName("node1"); + node1.setId(111); + MnyNode node2 = new MnyNode(); + node2.setName("node2"); + node2.setId(222); + MnyNode node3 = new MnyNode(); + node3.setName("node3"); + node3.setId(333); + MnyNode node4 = new MnyNode(); + node4.setName("node4"); + node4.setId(444); + + node1.getAllReverseRelations().add(node2); + node1.getAllRelations().add(node2); + node2.getAllRelations().add(node3); + node3.getAllRelations().add(node4); + DB.save(node1); + DB.save(node1); + + DB.refresh(node2); + DB.refresh(node3); + assertThat(node2.getAllRelations()).containsExactlyInAnyOrder(node1, node3); + assertThat(node3.getAllReverseRelations()).containsExactlyInAnyOrder(node2); + + DB.refresh(node1); + node1.getAllReverseRelations().clear(); + System.out.println("Clearing"); + DB.save(node1); + DB.refresh(node2); + assertThat(node2.getAllRelations()).containsExactlyInAnyOrder(node3); + + node2.getAllRelations().clear(); + node2.getAllRelations().add(node3); + LoggedSql.start(); + DB.save(node2); + LoggedSql.stop().forEach(System.out::println); + + } + + @Test + public void testAccessAndModify() throws Exception { + createTestData(); + + MnyNode node = DB.find(MnyNode.class, 1); + node.setName("fooBarBaz"); + MnyNode removed = node.getAllRelations().remove(0); + + LoggedSql.start(); + DB.save(node); + List<String> sql = LoggedSql.stop(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("update mny_node set name=? where id=?; -- bind(fooBarBaz"); + assertThat(sql.get(1)).contains("delete from mny_edge where from_id = ? and to_id = ? and mny_edge.flags != 12345 and 'mny_node' = 'mny_node'"); + assertThat(sql.get(2)).contains("-- bind"); + + node.getAllRelations().add(removed); + LoggedSql.start(); + DB.save(node); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("insert into mny_edge (id, flags, from_id, to_id) values (?,?,?,?)"); + assertThat(sql.get(1)).contains("-- bind"); + + } + @Test public void testQuery() throws Exception { createTestData(); @@ -70,9 +138,9 @@ public void testGetter() throws Exception { // prefetch everything LoggedSql.start(); node = DB.find(MnyNode.class) - .fetch("bit1Relations","*") - .fetch("bit1ReverseRelations","*") - .where().idEq(3).findOne(); + .fetch("bit1Relations", "*") + .fetch("bit1ReverseRelations", "*") + .where().idEq(3).findOne(); sqls = LoggedSql.stop(); assertThat(sqls).hasSize(2); diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java index d9a6944765..d7b5d39f62 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java @@ -10,7 +10,7 @@ public class OtoBMaster { String name; - @OneToOne(cascade = CascadeType.ALL, mappedBy = "master", fetch = FetchType.LAZY) + @OneToOne(cascade = CascadeType.ALL, mappedBy = "master", fetch = FetchType.LAZY, optional = false) OtoBChild child; public Long getId() { diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java index 36d7104c2d..afc224ab45 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java @@ -1,9 +1,6 @@ package org.tests.model.onetoone; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.OneToOne; -import javax.persistence.Version; +import javax.persistence.*; import java.util.UUID; @Entity @@ -17,7 +14,7 @@ public class OtoUBPrime { /** * Master side of bi-directional PrimaryJoinColumn. */ - @OneToOne(mappedBy = "prime") + @OneToOne(mappedBy = "prime", optional = false) OtoUBPrimeExtra extra; @Version diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java index df14dee017..ef8532a15f 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java @@ -14,7 +14,7 @@ public class OtoUBPrimeExtra { /** * Child side of bi-directional PrimaryJoinColumn. */ - @OneToOne + @OneToOne(optional = false) @PrimaryKeyJoinColumn OtoUBPrime prime; diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java index e4371304fa..f8ad34b0fd 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java @@ -13,15 +13,27 @@ public class OtoUPrime { String name; + /** * Effectively Ebean automatically sets Cascade PERSIST and mapped by for PrimaryKeyJoinColumn. - * This OneToOne is optional so left join to extra. + * This OneToOne is not optional so use inner join to extra (unless DbForeignkey(noConstraint = true) is set) + * Note: Violating the contract (Storing OtoUPrime without extra) may cause problems: + * - due the inner join, you might not get results from the query + * - you might get a "Beah has been deleted" if lazy load occurs on 'extra' */ - @OneToOne + @OneToOne(orphanRemoval = true, optional = false) @PrimaryKeyJoinColumn + // enforcing left join - without 'noConstraint = true', an inner join is used @DbForeignKey(noConstraint = true) OtoUPrimeExtra extra; + /** + * This OneToOne is optional so left join to extra. + * Setting FetchType.LAZY will NOT add the left join by default to the query. + */ + @OneToOne(mappedBy = "prime", fetch = FetchType.LAZY, orphanRemoval = true, optional = true) + OtoUPrimeOptionalExtra optionalExtra; + @Version Long version; @@ -65,4 +77,12 @@ public Long getVersion() { public void setVersion(Long version) { this.version = version; } + + public OtoUPrimeOptionalExtra getOptionalExtra() { + return optionalExtra; + } + + public void setOptionalExtra(OtoUPrimeOptionalExtra optionalExtra) { + this.optionalExtra = optionalExtra; + } } diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java index edd72b0ae3..0b2c0e8f8f 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java @@ -1,8 +1,8 @@ package org.tests.model.onetoone; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Version; +import io.ebean.annotation.Formula; + +import javax.persistence.*; import java.util.UUID; @Entity @@ -22,7 +22,7 @@ public OtoUPrimeExtra(String extra) { @Override public String toString() { - return "exId:"+ eid +" "+extra; + return "exId:" + eid + " " + extra; } public UUID getEid() { diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java new file mode 100644 index 0000000000..9b69a3e7c7 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java @@ -0,0 +1,52 @@ +package org.tests.model.onetoone; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.UUID; + +@Entity +public class OtoUPrimeExtraWithConstraint { + + @Id + UUID eid; + + String extra; + + @Version + Long version; + + public OtoUPrimeExtraWithConstraint(String extra) { + this.extra = extra; + } + + @Override + public String toString() { + return "exId:" + eid + " " + extra; + } + + public UUID getEid() { + return eid; + } + + public void setEid(UUID eid) { + this.eid = eid; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java new file mode 100644 index 0000000000..7ec0d0f1e3 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java @@ -0,0 +1,61 @@ +package org.tests.model.onetoone; + +import javax.persistence.*; +import java.util.UUID; + +@Entity +public class OtoUPrimeOptionalExtra { + + @Id + UUID eid; + + String extra; + + @OneToOne(optional = false) + @PrimaryKeyJoinColumn + private OtoUPrime prime; + + @Version + Long version; + + public OtoUPrimeOptionalExtra(String extra) { + this.extra = extra; + } + + @Override + public String toString() { + return "exId:" + eid + " " + extra; + } + + public UUID getEid() { + return eid; + } + + public void setEid(UUID eid) { + this.eid = eid; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public OtoUPrime getPrime() { + return prime; + } + + public void setPrime(OtoUPrime prime) { + this.prime = prime; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java new file mode 100644 index 0000000000..29babe5685 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java @@ -0,0 +1,63 @@ +package org.tests.model.onetoone; + +import javax.persistence.*; +import java.util.UUID; + +@Entity +public class OtoUPrimeWithConstraint { + + @Id + UUID pid; + + String name; + + @OneToOne(orphanRemoval = true, optional = false) + // @DbForeignKey(noConstraint = true) see OtoUPrime + @PrimaryKeyJoinColumn + OtoUPrimeExtraWithConstraint extra; + + @Version + Long version; + + public OtoUPrimeWithConstraint(String name) { + this.name = name; + } + + @Override + public String toString() { + return "id:" + pid + " name:" + name + " extra:" + extra; + } + + public UUID getPid() { + return pid; + } + + public void setPid(UUID pid) { + this.pid = pid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OtoUPrimeExtraWithConstraint getExtra() { + return extra; + } + + public void setExtra(OtoUPrimeExtraWithConstraint extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java index 122e298e93..1aa765092e 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java @@ -34,7 +34,7 @@ public void findWithLazyOneToOne() { String sql = sqlOf(query); assertThat(sql).contains("select t0.id, t0.name from oto_bmaster t0 where t0.id "); - assertThat(sql).doesNotContain("left join oto_bchild"); + assertThat(sql).doesNotContain("join oto_bchild"); assertThat(one).isNotNull(); diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java index 81e8905ed7..63dd38bdde 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java @@ -45,7 +45,7 @@ public void insertUpdateDelete() { OtoUBPrime oneWith = queryWithFetch.findOne(); assertThat(oneWith).isNotNull(); - assertThat(sqlOf(queryWithFetch, 10)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version, t1.eid from oto_ubprime t0 left join oto_ubprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") + assertThat(sqlOf(queryWithFetch, 10)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version, t1.eid from oto_ubprime t0 join oto_ubprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") .as("we join to oto_prime_extra"); assertThat(oneWith.getExtra().getExtra()).isEqualTo("v" + desc); diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java index 39cbfa7309..3a9a4d6df2 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java @@ -1,14 +1,21 @@ package org.tests.model.onetoone; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Query; +import io.ebean.plugin.Property; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import javax.persistence.EntityNotFoundException; +import javax.persistence.PersistenceException; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; public class TestOneToOnePrimaryKeyJoinOptional extends BaseTestCase { @@ -21,23 +28,120 @@ private OtoUPrime insert(String desc) { return prime; } - @Test - public void insertWithoutExtra() { + @BeforeEach + void prepare() { + OtoUPrime p1Single = new OtoUPrime("Prime without optional"); + p1Single.setExtra(new OtoUPrimeExtra("Non optional prime required")); + DB.save(p1Single); + + OtoUPrimeExtra p2 = new OtoUPrimeExtra("SinglePrimeExtra"); + try { + DB.save(p2); + fail("PrimExtra cannot exist without Prime"); + } catch (PersistenceException pe) { + + } String desc = "" + System.currentTimeMillis(); OtoUPrime p1 = new OtoUPrime("u" + desc); + p1.setExtra(new OtoUPrimeExtra("u" + desc)); + p1.setOptionalExtra(new OtoUPrimeOptionalExtra("This one has also an optional")); DB.save(p1); + } - Query<OtoUPrime> query = DB.find(OtoUPrime.class) - .setId(p1.getPid()) - .fetch("extra", "eid"); + @AfterEach + void cleanup() { + DB.find(OtoUPrime.class).delete(); + assertThat(DB.find(OtoUPrimeExtra.class).findList()).isEmpty(); + assertThat(DB.find(OtoUPrimeOptionalExtra.class).findList()).isEmpty(); + } - OtoUPrime found = query.findOne(); + public void doTest1(boolean extraFetch, boolean optionalFetch) { + + // Query for "fetch" case - extra bean joined by left join + + Query<OtoUPrime> query1 = DB.find(OtoUPrime.class); + if (extraFetch) { + query1.fetch("extra"); + } + if (optionalFetch) { + query1.fetch("optionalExtra"); + } + List<OtoUPrime> primes = query1.findList(); + if (extraFetch && optionalFetch) { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.version, " + + "t1.eid, t1.extra, t1.version, " + + "t2.eid, t2.extra, t2.version, t2.eid " + + "from oto_uprime t0 " + + "left join oto_uprime_extra t1 on t1.eid = t0.pid " + // left join on non-optional, because DbForeignKey(noConstraint=true) is set + "left join oto_uprime_optional_extra t2 on t2.eid = t0.pid"); // left join on optional + } else if (extraFetch) { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_uprime t0 left join oto_uprime_extra t1 on t1.eid = t0.pid"); + } else if (optionalFetch) { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.pid, t0.version, t1.eid, t1.extra, t1.version, t1.eid from oto_uprime t0 left join oto_uprime_optional_extra t1 on t1.eid = t0.pid"); + + } else { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.pid, t0.version from oto_uprime t0"); + } - if (found.getExtra() != null) { - found.getExtra().getExtra(); // fails here, because getExtra should be null + List<Long> versions = new ArrayList<>(); + for (OtoUPrime prime : primes) { + if (prime.getOptionalExtra() != null) { + versions.add(prime.getOptionalExtra().getVersion()); + } } - assertThat(found.getExtra()).isNull(); + assertThat(primes).hasSize(2); + assertThat(versions).containsExactly(1L); + } + + public void doTest2(boolean withFetch) { + + Query<OtoUPrimeOptionalExtra> query2 = DB.find(OtoUPrimeOptionalExtra.class); + if (withFetch) { + query2.fetch("prime"); + } + List<OtoUPrimeOptionalExtra> extraPrimes = query2.findList(); + if (withFetch) { + assertThat(query2.getGeneratedSql()).isEqualTo("select t0.eid, t0.extra, t0.version, t1.pid, t1.name, t1.pid, t1.version from oto_uprime_optional_extra t0 join oto_uprime t1 on t1.pid = t0.eid"); + } else { + assertThat(query2.getGeneratedSql()).isEqualTo("select t0.eid, t0.extra, t0.version, t0.eid from oto_uprime_optional_extra t0"); + } + List<Long> versions = new ArrayList<>(); + for (OtoUPrimeOptionalExtra extraPrime : extraPrimes) { + versions.add(extraPrime.getPrime().getVersion()); + } + assertThat(extraPrimes).hasSize(1); + assertThat(versions).containsExactly(1L); + } + + @Test + void testWithExtraFetch1() { + doTest1(true, false); + } + + @Test + void testWithOptionalFetch1() { + doTest1(false, true); + } + + @Test + void testWithBothFetch1() { + doTest1(true, true); + } + + @Test + void testWithoutFetch1() { + doTest1(false, false); + } + + @Test + void testWithFetch2() { + doTest2(true); + } + + @Test + void testWithoutFetch2() { + doTest2(false); } @Test @@ -54,7 +158,7 @@ public void insertUpdateDelete() { OtoUPrime found = query.findOne(); assertThat(found).isNotNull(); - assertThat(sqlOf(query, 4)).contains("select t0.pid, t0.name, t0.version, t0.pid from oto_uprime t0 where t0.pid = ?") + assertThat(sqlOf(query, 4)).contains("select t0.pid, t0.name, t0.pid, t0.version from oto_uprime t0 where t0.pid = ?") .as("we don't join to oto_uprime_extra"); assertThat(found.getName()).isEqualTo("u" + desc); @@ -66,7 +170,8 @@ public void insertUpdateDelete() { OtoUPrime oneWith = queryWithFetch.findOne(); assertThat(oneWith).isNotNull(); - assertThat(sqlOf(queryWithFetch, 6)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_uprime t0 left join oto_uprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") + assertThat(sqlOf(queryWithFetch, 6)) + .contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_uprime t0 left join oto_uprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") .as("we join to oto_prime_extra"); @@ -98,8 +203,77 @@ private void thenDelete(OtoUPrime found) { DB.delete(bean); List<String> sql = LoggedSql.stop(); - assertThat(sql).hasSize(2); + + assertThat(sql).hasSize(3); assertSql(sql.get(0)).contains("delete from oto_uprime_extra where"); - assertSql(sql.get(1)).contains("delete from oto_uprime where"); + assertSql(sql.get(1)).contains("delete from oto_uprime_optional_extra where"); + assertSql(sql.get(2)).contains("delete from oto_uprime where"); + } + + @Test + void testDdl() { + Collection<? extends Property> props = DB.getDefault().pluginApi().beanType(OtoUPrime.class).allProperties(); + + for (Property prop : props) { + System.out.println(prop); + } + } + + @Test + void testContractViolation1() { + + OtoUPrime p1 = new OtoUPrime("Prime having no extra"); + // extra is "optional=false" - and this is a violating of the contract + DB.save(p1); + + Query<OtoUPrime> query = DB.find(OtoUPrime.class).setId(p1.pid); + + OtoUPrime found1 = query.findOne(); + assertThat(query.getGeneratedSql()).doesNotContain("join"); + + assertThat(found1.getExtra()).isNotNull(); + assertThatThrownBy(() -> found1.getExtra().getVersion()).isInstanceOf(EntityNotFoundException.class); + + query.fetch("extra"); + OtoUPrime found2 = query.findOne(); + // Note: We use "left join" here, because 'DbForeignKey(noConstraint=true)' is set oh the property + // if this annotation is not preset, an inner join would be used and 'found2' would be 'null' then + assertThat(query.getGeneratedSql()).contains("from oto_uprime t0 left join oto_uprime_extra"); + assertThat(found2.getExtra()).isNull(); + + } + + @Test + void testContractViolation2() { + + OtoUPrimeExtraWithConstraint p1Const = new OtoUPrimeExtraWithConstraint("test"); + try { + // a foreign key prevents from saving + DB.save(p1Const); + fail("PrimExtra cannot exist without Prime"); + } catch (PersistenceException pe) { + // OK + } + + OtoUPrimeWithConstraint p1 = new OtoUPrimeWithConstraint("Prime having no extra"); + // extra is "optional=false" - and this is a violating of the contract + // Note there is no real foreign key in the database, that would prevent saving this entity + DB.save(p1); + + Query<OtoUPrimeWithConstraint> query = DB.find(OtoUPrimeWithConstraint.class).setId(p1.pid); + + OtoUPrimeWithConstraint found1 = query.findOne(); + assertThat(query.getGeneratedSql()).doesNotContain("join"); + + assertThat(found1.getExtra()).isNotNull(); + assertThatThrownBy(() -> found1.getExtra().getVersion()).isInstanceOf(EntityNotFoundException.class); + + query.fetch("extra"); + OtoUPrimeWithConstraint found2 = query.findOne(); + // Note: We use "left join" here, because 'DbForeignKey(noConstraint=true)' is set oh the property + // if this annotation is not preset, an inner join would be used and 'found2' would be 'null' then + assertThat(query.getGeneratedSql()).contains("from oto_uprime_with_constraint t0 join oto_uprime_extra_with_constraint"); + assertThat(found2).isNull(); + } } diff --git a/ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java b/ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java new file mode 100644 index 0000000000..3f392aed06 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java @@ -0,0 +1,61 @@ +package org.tests.model.tevent; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import io.ebean.annotation.Formula; +import io.ebean.config.dbplatform.DatabasePlatform; +import io.ebean.plugin.CustomDeployParser; +import io.ebean.plugin.DeployBeanDescriptorMeta; +import io.ebean.plugin.DeployBeanPropertyMeta; +import io.ebean.util.AnnotationUtil; +import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; + +/** + * Custom Annotation parser which parses @Count annotation + * + * @author Roland Praml, FOCONIS AG + */ +public class CustomFormulaAnnotationParser implements CustomDeployParser { + + private int counter; + + + @Target(FIELD) + @Retention(RUNTIME) + @Formula(select="TODO", join = "TODO") // meta-formula + public @interface Count { + String value(); + } + + + + @Override + public void parse(final DeployBeanDescriptorMeta descriptor, final DatabasePlatform databasePlatform) { + for (DeployBeanPropertyMeta prop : descriptor.propertiesAll()) { + readField(descriptor, prop); + } + } + + private void readField(DeployBeanDescriptorMeta descriptor, DeployBeanPropertyMeta prop) { + Count countAnnot = AnnotationUtil.get(prop.getField(), Count.class); + if (countAnnot != null) { + // @Count found, so build the (complex) count formula + DeployBeanPropertyAssocMany<?> countProp = (DeployBeanPropertyAssocMany<?>) descriptor.getBeanProperty(countAnnot.value()); + counter++; + String tmpTable = "f"+counter; + String sqlSelect = "coalesce(" + tmpTable + ".child_count, 0)"; + String parentId = countProp.getMappedBy() + "_id"; + String tableName = countProp.getBeanTable().getBaseTable(); + String sqlJoin = "left join (select " + parentId +", count(*) as child_count from " + tableName + " GROUP BY " + parentId + " )" + + " " + tmpTable + " on " + tmpTable + "." +parentId + " = ${ta}." + descriptor.idProperty().getDbColumn(); + prop.setSqlFormula(sqlSelect, sqlJoin); +// prop.setSqlFormula("f1.child_count", +// "join (select parent_id, count(*) as child_count from child_entity GROUP BY parent_id) f1 on f1.parent_id = ${ta}.id"); + } + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java b/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java index bcbad8ef2e..678ca9677c 100644 --- a/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java +++ b/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java @@ -42,6 +42,11 @@ public enum Status { @OneToMany(mappedBy = "event", cascade = CascadeType.ALL) List<TEventMany> logs; + @CustomFormulaAnnotationParser.Count("logs") + //@Formula(select = "f1.child_count", + //join = "left join (select event_id, count(*) as child_count from tevent_many GROUP BY event_id ) as f1 on f1.event_id = ${ta}.id") + Long customFormula; + public TEventOne(String name, Status status) { this.name = name; this.status = status; @@ -64,6 +69,10 @@ public Long getCount() { return count; } + public Long getCustomFormula() { + return customFormula; + } + public BigDecimal getTotalUnits() { return totalUnits; } diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/AbstractVirtualBase.java b/ebean-test/src/test/java/org/tests/model/virtualprop/AbstractVirtualBase.java new file mode 100644 index 0000000000..7842cd0bb0 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/AbstractVirtualBase.java @@ -0,0 +1,22 @@ +package org.tests.model.virtualprop; + +import io.ebean.bean.extend.ExtendableBean; + +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; + +@MappedSuperclass +public class AbstractVirtualBase implements ExtendableBean { + + @Id + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBase.java b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBase.java new file mode 100644 index 0000000000..cba2c5c8d8 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBase.java @@ -0,0 +1,17 @@ +package org.tests.model.virtualprop; + +import javax.persistence.*; + +@Entity +public class VirtualBase extends AbstractVirtualBase { + + private String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseA.java b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseA.java new file mode 100644 index 0000000000..4e3b2d2b76 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseA.java @@ -0,0 +1,19 @@ +package org.tests.model.virtualprop; + +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; + +@Entity +@DiscriminatorValue("A") +public class VirtualBaseA extends VirtualBaseInherit { + + private Integer num; + + public Integer getNum() { + return num; + } + + public void setNum(Integer num) { + this.num = num; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseB.java b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseB.java new file mode 100644 index 0000000000..17c86f2936 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseB.java @@ -0,0 +1,19 @@ +package org.tests.model.virtualprop; + +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; + +@Entity +@DiscriminatorValue("B") +public class VirtualBaseB extends VirtualBaseInherit { + + private String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseInherit.java b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseInherit.java new file mode 100644 index 0000000000..9dce83fc60 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseInherit.java @@ -0,0 +1,23 @@ +package org.tests.model.virtualprop; + +import javax.persistence.DiscriminatorColumn; +import javax.persistence.Entity; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; + +@Entity +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "kind") +public class VirtualBaseInherit extends AbstractVirtualBase { + + private String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension1.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension1.java new file mode 100644 index 0000000000..4a6a53ce0a --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension1.java @@ -0,0 +1,26 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.AbstractVirtualBase; + +/** + * This class will add the field 'ext' to 'VirtualBaseA' by EntityExtension + */ +@EntityExtension(AbstractVirtualBase.class) +public class Extension1 { + + private String ext; + + public String getExt() { + return ext; + } + + public void setExt(String ext) { + this.ext = ext; + } + + public static Extension1 get(AbstractVirtualBase base) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension2.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension2.java new file mode 100644 index 0000000000..ff0cf32663 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension2.java @@ -0,0 +1,30 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.AbstractVirtualBase; +import org.tests.model.virtualprop.VirtualBase; +import org.tests.model.virtualprop.VirtualBaseA; + +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import java.util.List; + +/** + * This class will add the fields 'virtualExtendManyToManys' to 'AbstractVirtualBase' by EntityExtension + */ +@EntityExtension(VirtualBase.class) +public class Extension2 { + + @ManyToMany + @JoinTable(name = "kreuztabelle") + private List<VirtualExtendManyToMany> virtualExtendManyToManys; + + public List<VirtualExtendManyToMany> getVirtualExtendManyToManys() { + return virtualExtendManyToManys; + } + + public static Extension2 get(VirtualBase found) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension3.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension3.java new file mode 100644 index 0000000000..876d9534e6 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension3.java @@ -0,0 +1,41 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.annotation.Formula; +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.VirtualBase; + +import javax.persistence.OneToOne; + +/** + * This class will add the fields 'virtualExtendOne' and 'firstName' to 'VirtualBase' by EntityExtension + */ +@EntityExtension(VirtualBase.class) +public class Extension3 { + + @OneToOne(mappedBy = "base") + private VirtualExtendOne virtualExtendOne; + + @Formula(select = "concat('Your name is ', ${ta}.data)") + private String firstName; + + public static Extension3 get(VirtualBase found) { + throw new NotEnhancedException(); + } + + public VirtualExtendOne getVirtualExtendOne() { + return virtualExtendOne; + } + + public void setVirtualExtendOne(VirtualExtendOne virtualExtendOne) { + this.virtualExtendOne = virtualExtendOne; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension4.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension4.java new file mode 100644 index 0000000000..8b8ee66cbc --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension4.java @@ -0,0 +1,26 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.VirtualBaseA; + +/** + * This class will add the field 'ext' to 'VirtualBaseA' by EntityExtension + */ +@EntityExtension(VirtualBaseA.class) +public class Extension4 { + + private String extA; + + public String getExtA() { + return extA; + } + + public void setExtA(String extA) { + this.extA = extA; + } + + public static Extension4 get(VirtualBaseA base) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension5.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension5.java new file mode 100644 index 0000000000..8000139ea7 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension5.java @@ -0,0 +1,24 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.VirtualBaseA; + +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import java.util.ArrayList; +import java.util.List; + +/** + * This class will add the field 'ext' to 'VirtualBaseA' by EntityExtension + */ +@EntityExtension(VirtualBaseA.class) +public class Extension5 { + + @OneToMany + private List<VirtualAExtendOne> virtualExtends = new ArrayList<>(); + + public static Extension5 get(VirtualBaseA base) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension6.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension6.java new file mode 100644 index 0000000000..2bbb1bca91 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension6.java @@ -0,0 +1,35 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.annotation.DbJson; +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.VirtualBaseA; + +import javax.persistence.OneToMany; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * This class will add the field 'nums' to 'VirtualBaseA' by EntityExtension + */ +@EntityExtension(VirtualBaseA.class) +public class Extension6 { + + @DbJson + private Set<Integer> nums = new LinkedHashSet<>(); + + public Set<Integer> getNums() { + return nums; + } + + public void setNums(Set<Integer> nums) { + this.nums = nums; + } + + public static Extension6 get(VirtualBaseA base) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/TestVirtualProps.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/TestVirtualProps.java new file mode 100644 index 0000000000..2d79efac85 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/TestVirtualProps.java @@ -0,0 +1,181 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.DB; +import io.ebean.Database; +import io.ebean.plugin.BeanType; +import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import io.ebeaninternal.server.deploy.BeanProperty; +import org.junit.jupiter.api.Test; +import org.tests.model.virtualprop.VirtualBase; +import org.tests.model.virtualprop.VirtualBaseA; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Demo, how to use virtual properties. Note: that there is + * @author Roland Praml, FOCONIS AG + */ +public class TestVirtualProps extends BaseTestCase { + static Database db = DB.getDefault(); + + /* + private static Database createDb() { + DatabaseConfig config = new DatabaseConfig(); + config.setName("h2"); + config.loadFromProperties(); + config.setDdlExtra(false); + config.setPackages(List.of("org.tests.model.virtualprop")); + return DatabaseFactory.create(config); + }*/ + + @Test + void testCreate() { + + //DB.getDefault(); // Init database to start parser + VirtualBase base = new VirtualBase(); + base.setData("Foo"); + + db.save(base); + + VirtualBase found = db.find(VirtualBase.class).where().isNull("virtualExtendOne").findOne(); + assertThat(found).isNotNull(); + + found = db.find(VirtualBase.class).where().isNotNull("virtualExtendOne").findOne(); + assertThat(found).isNull(); + + BeanType<VirtualBase> bt = db.pluginApi().beanType(VirtualBase.class); + BeanProperty prop = (BeanProperty) bt.property("virtualExtendOne"); + + + found = db.find(VirtualBase.class).where().isNull("virtualExtendOne").findOne(); + VirtualExtendOne ext = new VirtualExtendOne(); + ext.setData("bar"); + + prop.pathSet(found, ext); + db.save(found); + + Extension3 other = Extension3.get(found); + assertThat(other.getVirtualExtendOne().getData()).isEqualTo("bar"); + other.setFirstName("test"); + + Extension2 many = Extension2.get(found); + assertThat(many.getVirtualExtendManyToManys()).isEmpty(); + other.getVirtualExtendOne().setData("faz"); + db.save(found); + + found = db.find(VirtualBase.class).where().eq("virtualExtendOne.data", "faz").findOne(); + assertThat(found).isNotNull(); + + List<Object> attr = db.find(VirtualBase.class).fetch("virtualExtendOne", "data").findSingleAttributeList(); + assertThat(attr).containsExactly("faz"); + + attr = db.find(VirtualBase.class).select("firstName").findSingleAttributeList(); + assertThat(attr).containsExactly("Your name is Foo"); + VirtualExtendOne oneFound = (VirtualExtendOne) prop.pathGet(found); + assertThat(oneFound.getData()).isEqualTo("faz"); + + db.delete(oneFound); // cleanup + } + + @Test + void testCreateMany() { + + VirtualBase base1 = new VirtualBase(); + base1.setData("Foo"); + db.save(base1); + + VirtualBase base2 = new VirtualBase(); + base2.setData("Bar"); + db.save(base2); + + VirtualExtendManyToMany many1 = new VirtualExtendManyToMany(); + many1.setData("Alex"); + db.save(many1); + + VirtualExtendManyToMany many2 = new VirtualExtendManyToMany(); + many2.setData("Roland"); + db.save(many2); + + BeanType<VirtualBase> bt = db.pluginApi().beanType(VirtualBase.class); + BeanProperty prop = (BeanProperty) bt.property("virtualExtendManyToManys"); + List<VirtualExtendManyToMany> list = (List<VirtualExtendManyToMany>) prop.pathGet(base1); + assertThat(list).isEmpty(); + list.add(many1); + LoggedSql.start(); + db.save(base1); + List<String> sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("insert into kreuztabelle (virtual_base_id, virtual_extend_many_to_many_id) values (?, ?)"); + assertThat(sql.get(1)).contains("-- bind"); + + many2.getBases().add(base1); + LoggedSql.start(); + db.save(many2); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + + + LoggedSql.start(); + VirtualBase found = db.find(VirtualBase.class, base1.getId()); + list = (List<VirtualExtendManyToMany>) prop.pathGet(found); + assertThat(list).hasSize(2).containsExactlyInAnyOrder(many1, many2); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); +// assertThat(sql.get(0)).contains("select t0.id, t0.data, concat('Your name is ', t0.data), t1.id from virtual_base t0 left join virtual_extend_one t1 on t1.id = t0.id where t0.id = ?"); + if (isPostgresCompatible()) { + assertThat(sql.get(1)).contains("select int_.virtual_base_id, t0.id, t0.data from virtual_extend_many_to_many t0 left join kreuztabelle int_ on int_.virtual_extend_many_to_many_id = t0.id where (int_.virtual_base_id) = any(?)"); + } else { + assertThat(sql.get(1)).contains("select int_.virtual_base_id, t0.id, t0.data from virtual_extend_many_to_many t0 left join kreuztabelle int_ on int_.virtual_extend_many_to_many_id = t0.id where (int_.virtual_base_id) in (?)"); + } + DB.find(VirtualBase.class).delete(); + DB.find(VirtualExtendManyToMany.class).delete(); + } + + @Test + void testCreateDelete() { + + VirtualBase base = new VirtualBase(); + base.setData("Master"); + db.save(base); + + VirtualExtendOne extendOne = new VirtualExtendOne(); + extendOne.setBase(base); + extendOne.setData("Extended"); + db.save(extendOne); + + VirtualBase found = db.find(VirtualBase.class, base.getId()); + + LoggedSql.start(); + db.delete(found); + List<String> sql = LoggedSql.stop(); + + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("delete from virtual_extend_one where id = ?"); // delete OneToOne - why 'id=?' and not 'id = ?' + assertThat(sql.get(1)).contains("delete from kreuztabelle where virtual_base_id = ?"); // intersection table + assertThat(sql.get(2)).contains("delete from virtual_base where id=?"); // delete entity itself + + } + + @Test + void testInheritance() { + + VirtualBaseA base = new VirtualBaseA(); + base.setData("Master"); + Extension1.get(base).setExt("ext"); + //db.save(base); + + Extension4.get(base).setExtA("extA"); + db.save(base); + + LoggedSql.start(); + VirtualBaseA found = db.find(VirtualBaseA.class).where().eq("extA", "extA").findOne(); + List<String> sql = LoggedSql.stop(); + + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.kind, t0.id, t0.data, t0.num, t0.ext, t0.ext_a, t0.nums from virtual_base_inherit t0 where t0.kind = 'A' and t0.ext_a = ?"); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualAExtendOne.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualAExtendOne.java new file mode 100644 index 0000000000..6bc41a156a --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualAExtendOne.java @@ -0,0 +1,44 @@ +package org.tests.model.virtualprop.ext; + +import org.tests.model.virtualprop.VirtualBaseA; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; + +@Entity +public class VirtualAExtendOne { + + @Id + private int id; + + private String data; + + @ManyToOne + private VirtualBaseA base; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public VirtualBaseA getBase() { + return base; + } + + public void setBase(VirtualBaseA base) { + this.base = base; + this.id = base == null ? 0 : base.getId(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendManyToMany.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendManyToMany.java new file mode 100644 index 0000000000..ae0573e1b2 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendManyToMany.java @@ -0,0 +1,43 @@ +package org.tests.model.virtualprop.ext; + +import org.tests.model.virtualprop.VirtualBase; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import java.util.List; + +@Entity +public class VirtualExtendManyToMany { + @Id + private int id; + + private String data; + + @ManyToMany(mappedBy = "virtualExtendManyToManys") + private List<VirtualBase> bases; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public List<VirtualBase> getBases() { + return bases; + } + + public void setBases(List<VirtualBase> bases) { + this.bases = bases; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendOne.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendOne.java new file mode 100644 index 0000000000..28c16e4ba6 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendOne.java @@ -0,0 +1,46 @@ +package org.tests.model.virtualprop.ext; + +import org.tests.model.virtualprop.VirtualBase; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToOne; +import javax.persistence.PrimaryKeyJoinColumn; + +@Entity +public class VirtualExtendOne { + + @Id + private int id; + + private String data; + + @PrimaryKeyJoinColumn + @OneToOne(optional = false) + private VirtualBase base; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public VirtualBase getBase() { + return base; + } + + public void setBase(VirtualBase base) { + this.base = base; + this.id = base == null ? 0 : base.getId(); + } +} diff --git a/ebean-test/src/test/java/org/tests/query/TestQueryFindEachHeapPressure.java b/ebean-test/src/test/java/org/tests/query/TestQueryFindEachHeapPressure.java new file mode 100644 index 0000000000..f164d2dde0 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/query/TestQueryFindEachHeapPressure.java @@ -0,0 +1,67 @@ +package org.tests.query; + +import io.ebean.DB; +import io.ebean.Transaction; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.tests.m2m.softdelete.MsManyA; +import org.tests.m2m.softdelete.MsManyB; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestQueryFindEachHeapPressure extends BaseTestCase { + + private byte[] buffer; + + @BeforeEach + public void before() { + DB.find(MsManyB.class).delete(); + DB.find(MsManyA.class).delete(); + + List<MsManyB> children = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + children.add(new MsManyB("child_" + i)); + } + DB.saveAll(children); + + try (Transaction txn = DB.beginTransaction()) { + for (int i = 0; i < 100; i++) { + MsManyA parent = new MsManyA("parent_" + i); + + Collections.shuffle(children); + + parent.getManybs().add(children.get(0)); + + DB.save(parent); + } + txn.commit(); + } + } + + /** + * Reproduce quickly, by manually adding System.gc at the beginning of CQuery#setLazyLoadedChildBean. + */ + @Test + @Disabled("Run manually with -Xmx128M") + public void test() throws Exception { + for (int j = 0; j < 1000; j++) { + System.out.println("Iteration " + j); + DB.find(MsManyA.class) + .findEach(parent -> { + if (Math.random() > 0.9) { + buffer = new byte[1_000_000]; + List<MsManyB> children = parent.getManybs(); + assertThat(children.size()).isEqualTo(1); + assertThat(buffer.length).isEqualTo(1_000_000); + } + }); + } + } + +} diff --git a/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java b/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java index 7d2bc67791..52e7d2237a 100644 --- a/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java +++ b/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java @@ -49,7 +49,7 @@ public void testBaseSelect() { List<TEventOne> list = query.findList(); String sql = sqlOf(query, 5); - assertThat(sql).contains("select t0.id, t0.name, t0.status, t0.version, t0.event_id from tevent_one t0"); + assertThat(sql).contains("select t0.id, t0.name, t0.status, coalesce(f1.child_count, 0), t0.version, t0.event_id from tevent_one t0"); for (TEventOne eventOne : list) { // lazy loading on Aggregation properties diff --git a/ebean-test/src/test/java/org/tests/query/finder/TestCustomerFinder.java b/ebean-test/src/test/java/org/tests/query/finder/TestCustomerFinder.java index 8b02734242..d25c124923 100644 --- a/ebean-test/src/test/java/org/tests/query/finder/TestCustomerFinder.java +++ b/ebean-test/src/test/java/org/tests/query/finder/TestCustomerFinder.java @@ -1,17 +1,22 @@ package org.tests.query.finder; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Transaction; -import io.ebean.xtest.IgnorePlatform; import io.ebean.annotation.Platform; import io.ebean.meta.*; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import io.ebean.xtest.IgnorePlatform; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.EBasic; import org.tests.model.basic.ResetBasicData; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -164,26 +169,51 @@ public void test_nativeSingleAttribute() { assertThat(names).isNotEmpty(); } + + @Test + @Disabled // run manually to generate sample report + public void test_report_queryPlans() throws IOException { + + ResetBasicData.reset(); + + // change default collect query plan threshold to 200 micros + MetricReportGenerator generator = server().metaInfo().createReportGenerator(); + generator.configure(List.of( + new MetricReportValue("initRequest.isAll", 1), + new MetricReportValue("initRequest.thresholdMicros", 2), + new MetricReportValue("initRequest.apply", 1))); + + // the server has some plans + runQueries(); + + generator.configure(List.of(new MetricReportValue("queryRequest.apply", 2))); + + File file = new File("sample-reports/report-" + server().name() + ".html"); + try (OutputStream out = new FileOutputStream(file)) { + generator.writeReport(out); + } + } + @Test @IgnorePlatform(Platform.DB2) // no query plans yet, for DB2 public void test_finders_queryPlans() { ResetBasicData.reset(); - // change default collect query plan threshold to 200 micros + // change default collect query plan threshold to 20 micros QueryPlanInit init0 = new QueryPlanInit(); init0.setAll(true); - init0.thresholdMicros(2); + init0.thresholdMicros(20); final List<MetaQueryPlan> plans = server().metaInfo().queryPlanInit(init0); assertThat(plans.size()).isGreaterThan(1); // the server has some plans runQueries(); - // change query plan threshold to 100 micros + // change query plan threshold to 10 micros QueryPlanInit init = new QueryPlanInit(); init.setAll(true); - init.thresholdMicros(1); + init.thresholdMicros(10); final List<MetaQueryPlan> appliedToPlans = server().metaInfo().queryPlanInit(init); assertThat(appliedToPlans.size()).isGreaterThan(4); @@ -212,9 +242,9 @@ public void test_finders_queryPlans() { List<MetaQueryPlan> plans0 = server().metaInfo().queryPlanCollectNow(request); assertThat(plans0).isNotEmpty(); - for (MetaQueryPlan plan : plans) { - logger.info("queryPlan label:{}, queryTimeMicros:{} loc:{} sql:{} bind:{} plan:{}", - plan.label(), plan.queryTimeMicros(), plan.profileLocation(), + for (MetaQueryPlan plan : plans0) { + logger.info("queryPlan label:{}, queryTimeMicros:{} captureMicros:{} whenCaptured:{} captureCount:{} loc:{} sql:{} bind:{} plan:{}", + plan.label(), plan.queryTimeMicros(), plan.captureMicros(), plan.whenCaptured(), plan.captureCount(), plan.profileLocation(), plan.sql(), plan.bind(), plan.plan()); System.out.println(plan); } diff --git a/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java b/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java index 66c2a76162..cf7ced8fbe 100644 --- a/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java +++ b/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java @@ -1,14 +1,15 @@ package org.tests.query.joins; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Query; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.tests.model.basic.Order; import org.tests.model.basic.OrderShipment; import org.tests.model.basic.ResetBasicData; +import org.tests.model.basic.TreeNode; import org.tests.model.family.ChildPerson; import org.tests.model.family.ParentPerson; @@ -31,8 +32,8 @@ public void test_OrderFindIds() { LoggedSql.start(); List<Integer> orderIds = DB.find(Order.class) - .where().eq("totalItems", 3) - .findIds(); + .where().eq("totalItems", 3) + .findIds(); assertThat(orderIds).hasSize(2); List<String> loggedSql = LoggedSql.stop(); @@ -46,8 +47,8 @@ public void test_OrderFindList() { LoggedSql.start(); List<Order> orders = DB.find(Order.class) - .where().eq("totalItems", 3) - .findList(); + .where().eq("totalItems", 3) + .findList(); assertThat(orders).hasSize(2); List<String> loggedSql = LoggedSql.stop(); @@ -122,11 +123,11 @@ public void testWhereOnChainedFormulaManyWhere() { } assertThat(shipQuery.getGeneratedSql()).contains( "from or_order_ship t0 " + - "join o_order u1 on u1.id = t0.order_id " + - "join or_order_ship u2 on u2.order_id = u1.id " + - "join o_order u3 on u3.id = u2.order_id " + - "left join (select order_id, count(*) as total_items, sum(order_qty*unit_price) as total_amount from o_order_detail group by order_id) z_bu3 on z_bu3.order_id = u3.id " + - "where z_bu3.total_amount is not null"); + "join o_order u1 on u1.id = t0.order_id " + + "join or_order_ship u2 on u2.order_id = u1.id " + + "join o_order u3 on u3.id = u2.order_id " + + "left join (select order_id, count(*) as total_items, sum(order_qty*unit_price) as total_amount from o_order_detail group by order_id) z_bu3 on z_bu3.order_id = u3.id " + + "where z_bu3.total_amount is not null"); } @Test @@ -188,9 +189,9 @@ public void test_OrderFindSingleAttributeList() { LoggedSql.start(); List<Date> orderDates = DB.find(Order.class) - .select("orderDate") - .where().eq("totalItems", 3) - .findSingleAttributeList(); + .select("orderDate") + .where().eq("totalItems", 3) + .findSingleAttributeList(); assertThat(orderDates).hasSize(2); List<String> sql = LoggedSql.stop(); @@ -205,11 +206,11 @@ public void test_OrderFindOne() { LoggedSql.start(); Order order = DB.find(Order.class) - .select("totalItems") - .where().eq("totalItems", 3) - .setMaxRows(1) - .orderById(true) - .findOne(); + .select("totalItems") + .where().eq("totalItems", 3) + .setMaxRows(1) + .orderById(true) + .findOne(); assertThat(order.getTotalItems()).isEqualTo(3); @@ -229,8 +230,8 @@ public void test_ParentPersonFindIds() { LoggedSql.start(); List<ParentPerson> orderIds = DB.find(ParentPerson.class) - .where().eq("totalAge", 3) - .findIds(); + .where().eq("totalAge", 3) + .findIds(); List<String> loggedSql = LoggedSql.stop(); assertEquals(1, loggedSql.size()); @@ -243,10 +244,10 @@ public void test_ParentPersonFindList() { LoggedSql.start(); DB.find(ParentPerson.class) - .select("identifier") - //.where().eq("totalAge", 3) - .where().eq("familyName", "foo") - .findList(); + .select("identifier") + //.where().eq("totalAge", 3) + .where().eq("familyName", "foo") + .findList(); List<String> sql = LoggedSql.stop(); assertEquals(1, sql.size()); @@ -323,12 +324,140 @@ public void test_ChildPersonParentFindCount() { LoggedSql.start(); DB.find(ChildPerson.class) - .where().eq("parent.totalAge", 3) - .findCount(); + .where().eq("parent.totalAge", 3) + .findCount(); List<String> loggedSql = LoggedSql.stop(); assertEquals(1, loggedSql.size()); assertThat(loggedSql.get(0)).contains("select count(*) from child_person t0 left join parent_person t1 on t1.identifier = t0.parent_identifier"); assertThat(loggedSql.get(0)).contains("where coalesce(f2.child_age, 0) = ?"); } + + @Test + public void test_softRef() { + + LoggedSql.start(); + + DB.find(TreeNode.class) + .where().isNotNull("ref.id") + .findList(); + + List<String> loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)).contains("select t0.id, t0.soft_ref, t0.parent_id, t0_ref.id " + + "from tree_node t0 " + + "left join e_basic t0_ref on t0_ref.id = t0.soft_ref " + + "left join e_basic t1 on t1.id = t0_ref.id where t1.id is not null"); + } + + @Test + public void test_softRefChildren() { + + LoggedSql.start(); + + DB.find(TreeNode.class).select("id") + .where().isNotNull("children.ref.id") + .findList(); + + List<String> loggedSql = LoggedSql.stop(); + if (isPostgresCompatible()) { + // TBD + } else { + assertThat(loggedSql.get(0)).contains("select distinct t0.id " + + "from tree_node t0 " + + "join tree_node u1 on u1.parent_id = t0.id " + + "join e_basic u1_ref on u1_ref.id = u1.soft_ref " + + "join e_basic u2 on u2.id = u1_ref.id where u2.id is not null"); + } + LoggedSql.start(); + DB.find(TreeNode.class).select("id") + .where().isNull("children.ref.id") + .findList(); + + loggedSql = LoggedSql.stop(); + if (isPostgresCompatible()) { + // TBD + } else { + assertThat(loggedSql.get(0)).contains("select distinct t0.id " + + "from tree_node t0 " + + "left join tree_node u1 on u1.parent_id = t0.id " + + "left join e_basic u1_ref on u1_ref.id = u1.soft_ref " + + "left join e_basic u2 on u2.id = u1_ref.id where u2.id is null"); + } + } + + @Test + public void test_fetch_only() { + + LoggedSql.start(); + + DB.find(ChildPerson.class).select("name").fetch("parent.effectiveBean").findList(); + + List<String> loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)).contains("from child_person t0 " + + "left join parent_person t1 on t1.identifier = t0.parent_identifier " + + "left join grand_parent_person j1 on j1.identifier = t1.parent_identifier " + + "left join e_basic t2 on t2.id = coalesce(t1.some_bean_id, j1.some_bean_id)"); + } + + @Test + public void test_where_only() { + + LoggedSql.start(); + + DB.find(ChildPerson.class).select("name") + .where().eq("parent.effectiveBean.name", "foo") + .findList(); + + List<String> loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)) + .contains("from child_person t0 " + + "left join parent_person t1 on t1.identifier = t0.parent_identifier " + + "left join grand_parent_person j1 on j1.identifier = t1.parent_identifier " + + "left join e_basic t2 on t2.id = coalesce(t1.some_bean_id, j1.some_bean_id) " + + "where t2.name = ?"); + } + + @Test + public void test_fetch_one_prop_with_where() { + + LoggedSql.start(); + // #2773: Ebean produces a wrong join, when one half comes from the fetch + // and the other half comes from the where + // fetching both properties or none will produce a correct query + DB.find(ChildPerson.class).select("name") + .fetch("parent", "name") + //.fetch("parent.effectiveBean", "name") + .where().eq("parent.effectiveBean.name", "foo") + .findList(); + + List<String> loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)) + .contains("from child_person t0 " + + "left join parent_person t1 on t1.identifier = t0.parent_identifier " + + "left join grand_parent_person j1 on j1.identifier = t1.parent_identifier " + + "left join e_basic t2 on t2.id = coalesce(t1.some_bean_id, j1.some_bean_id)" + + " where t2.name = ?" + ); + } + + @Test + public void test_fetch_complete_with_where() { + + LoggedSql.start(); + + DB.find(ChildPerson.class).select("name") + .fetch("parent") + .where().eq("parent.effectiveBean.name", "foo") + .findList(); + + List<String> loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)) + .contains("from child_person t0 " + + "left join parent_person t1 on t1.identifier = t0.parent_identifier " + + "left join (select i2.parent_identifier, count(*) as child_count, sum(i2.age) as child_age from child_person i2 group by i2.parent_identifier) f2 on f2.parent_identifier = t1.identifier " + + "left join grand_parent_person j1 on j1.identifier = t1.parent_identifier " + + "left join e_basic t2 on t2.id = coalesce(t1.some_bean_id, j1.some_bean_id) " + + "where t2.name = ?"); + } + } diff --git a/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java b/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java index db02e5edaa..3700900334 100644 --- a/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java +++ b/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java @@ -825,19 +825,24 @@ void setup() { e3.setAttr1("a1"); DB.save(e3); + Cat cat = new Cat(); + cat.setId(4711L); MainEntityRelation rel = new MainEntityRelation(); rel.setEntity1(e1); rel.setEntity2(e1); + rel.setCat2(cat); DB.save(rel); rel = new MainEntityRelation(); rel.setEntity1(e2); rel.setEntity2(e2); + rel.setCat2(cat); DB.save(rel); rel = new MainEntityRelation(); rel.setEntity1(e3); rel.setEntity2(e3); + rel.setCat2(cat); DB.save(rel); } diff --git a/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java b/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java index 7225f69e4e..4acb11bcae 100644 --- a/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java +++ b/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java @@ -1,8 +1,12 @@ package org.tests.text.json; import com.fasterxml.jackson.core.JsonParser; +import io.ebean.BeanMergeOptions; import io.ebean.BeanState; import io.ebean.DB; +import io.ebean.ValuePair; +import io.ebean.test.LoggedSql; +import io.ebean.text.json.JsonReadOptions; import io.ebean.xtest.BaseTestCase; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.json.SpiJsonReader; @@ -11,20 +15,17 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.tests.model.basic.Address; -import org.tests.model.basic.Contact; -import org.tests.model.basic.ContactNote; -import org.tests.model.basic.Customer; +import org.tests.model.basic.*; import java.io.IOException; import java.io.StringReader; import java.util.Comparator; +import java.util.List; +import java.util.Map; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class TestJsonBeanDescriptorParse extends BaseTestCase { @@ -34,6 +35,7 @@ void setup() { customer.setName("Hello Roland"); customer.setId(234); Address address = new Address(); + address.setId(234); address.setLine1("foo"); DB.save(address); customer.setBillingAddress(address); @@ -54,6 +56,7 @@ void setup() { @AfterEach void teardown() { DB.delete(Customer.class, 234); + DB.delete(UTMaster.class, 1); } @Test @@ -62,7 +65,7 @@ public void testJsonRead() throws IOException { BeanDescriptor<Customer> descriptor = server.descriptor(Customer.class); SpiJsonReader readJson = createRead(server, descriptor); - Customer customer = descriptor.jsonRead(readJson, null, null); + Customer customer = descriptor.jsonRead(readJson, null); assertEquals(Integer.valueOf(123), customer.getId()); assertEquals("Hello Rob", customer.getName()); @@ -87,7 +90,15 @@ public void testJsonManyUpdate() throws IOException { + " ]}," + " {\"id\": 790, \"firstName\": \"Anton\", \"lastName\": null, \"notes\" : null } " + "]}"; - DB.json().toBean(customer, json); + + BeanMergeOptions opts = new BeanMergeOptions(); + // example for a merge handler + // opts.setMergeHandler((a, b, c, d) -> { + // System.out.println("Merge " + d + "." +c); + // return true; + // }); + Customer jsonBean = DB.json().toBean(Customer.class, json); + DB.getDefault().mergeBeans(jsonBean, customer, null); DB.save(customer); customer = DB.find(Customer.class, 234); @@ -125,9 +136,15 @@ public void testJsonUpdate() throws IOException { assertEquals(null, customer.getName()); assertEquals("foo", customer.getBillingAddress().getLine1()); - DB.json().toBean(customer, "{\"billingAddress\":{\"line1\":\"foo\"}}"); + JsonReadOptions opts = new JsonReadOptions(); + opts.setEnableLazyLoading(true); + + Customer jsonBean = DB.json().toBean(Customer.class, "{\"billingAddress\":{\"line1\":\"foo\"}}", opts); + DB.getDefault().mergeBeans(jsonBean, customer, null); assertFalse(DB.beanState(customer.getBillingAddress()).isDirty()); - DB.json().toBean(customer, "{\"billingAddress\":{\"line1\":\"bar\"}}"); + + jsonBean = DB.json().toBean(Customer.class, "{\"billingAddress\":{\"line1\":\"bar\"}}", opts); + DB.getDefault().mergeBeans(jsonBean, customer, null); assertEquals("bar", customer.getBillingAddress().getLine1()); assertTrue(DB.beanState(customer.getBillingAddress()).isDirty()); @@ -180,11 +197,90 @@ public void testJsonUpdate() throws IOException { DB.delete(customer); // cleanup*/ } + @Test + public void testJsonLazyRead() throws IOException { + JsonReadOptions opts = new JsonReadOptions(); + opts.setEnableLazyLoading(true); + Customer customer = DB.json().toBean(Customer.class, "{\"id\": 234}", opts); + assertThat(customer.getBillingAddress().getLine1()).isEqualTo("foo"); + + } + + @Test + public void testJsonCollectionUnpopulated() throws IOException { + + Customer customer = DB.json().toBean(Customer.class, "{\"id\": 234, \"name\" : \"Roland\"}"); + + assertThat(customer.getBillingAddress()).isNull(); + List<Contact> contacts = customer.getContacts(); + + assertThat(contacts).isEmpty(); + ; + + } + + @Test + public void testJsonUpdateManyToOne() throws IOException { + Customer customer = DB.find(Customer.class, 234); + assertThat(customer.getBillingAddress().getLine1()).isEqualTo("foo"); + + Address address = new Address(); + address.setId(987); + address.setLine1("bar"); + DB.save(address); + customer.setBillingAddress(address); + DB.save(customer); + + assertThat(customer.getBillingAddress().getLine1()).isEqualTo("bar"); + JsonReadOptions opts = new JsonReadOptions(); + + LoggedSql.start(); + Customer jsonBean = DB.json().toBean(Customer.class, "{\"billingAddress\":{\"id\": 234, \"line1\" : \"bar\"}}", opts); + DB.getDefault().mergeBeans(jsonBean, customer, null); + assertThat(LoggedSql.stop()).isEmpty(); + + Map<String, ValuePair> dirty = DB.beanState(customer.getBillingAddress()).dirtyValues(); + assertThat(dirty).containsKeys("line1").hasSize(1); + assertThat(customer.getBillingAddress().getLine1()).isEqualTo("bar"); + assertThat(dirty.get("line1").getOldValue()).isEqualTo("foo"); + + } + + @Test + public void testJsonUpdateWithDbJson() { + UTMaster master = new UTMaster("m0"); + master.setId(1); + master.setJournal(new UTMaster.Journal()); + master.getJournal().addEntry(); + master.getJournal().addEntry(); + DB.save(master); + + DB.json().toBean(master, "{\"id\":1,\"name\":\"newName\",\"description\":\"master\",\"journal\":{\"entries\":[\"newEntry\"]},\"details\":[],\"version\":1}"); + + assertThat(master.getName()).isEqualTo("newName"); + assertThat(master.getDescription()).isEqualTo("master"); + assertThat(master.getJournal().getEntries()).hasSize(1); + + DB.json().toBean(master, "{\"id\":1,\"name\":\"name\",\"journal\":{}}"); + + assertThat(master.getName()).isEqualTo("name"); + assertThat(master.getDescription()).isEqualTo("master"); + assertThat(master.getJournal().getEntries()).hasSize(0); + + DB.json().toBean(master, "{\"id\":1,\"name\":\"newName\",\"description\":\"master\",\"journal\":{\"entries\":[\"newEntry\"]},\"details\":[],\"version\":1}"); + + assertThat(master.getName()).isEqualTo("newName"); + assertThat(master.getDescription()).isEqualTo("master"); + assertThat(master.getJournal().getEntries()).hasSize(1); + + + } + private SpiJsonReader createRead(SpiEbeanServer server, BeanDescriptor<Customer> descriptor) { StringReader reader = new StringReader("{\"id\":123,\"name\":\"Hello Rob\"}"); JsonParser parser = server.json().createParser(reader); - SpiJsonReader readJson = new ReadJson(descriptor, parser, null, null, false); + SpiJsonReader readJson = new ReadJson(descriptor, parser, null, null); return readJson; } diff --git a/ebean-test/src/test/java/org/tests/text/json/TestTextJsonInheritance.java b/ebean-test/src/test/java/org/tests/text/json/TestTextJsonInheritance.java index 6f2fc22f1a..da1e90f32f 100644 --- a/ebean-test/src/test/java/org/tests/text/json/TestTextJsonInheritance.java +++ b/ebean-test/src/test/java/org/tests/text/json/TestTextJsonInheritance.java @@ -1,12 +1,20 @@ package org.tests.text.json; -import io.ebean.xtest.BaseTestCase; +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.ObjectMapper; import io.ebean.DB; import io.ebean.text.json.JsonContext; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.tests.model.basic.*; +import java.io.*; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -40,6 +48,73 @@ public void test() { assertEquals(2, rebuiltList.size()); } + @Test + @Disabled("Run manually") + public void testPerformance() throws IOException { + for (int i = 0; i < 100000; i++) { + Truck c = new Truck(); + c.setLicenseNumber("L " + i); + c.setCapacity(20D); + DB.save(c); + } + DB.update(Truck.class).set("capacity", 40D).update(); + List<Vehicle> list = DB.find(Vehicle.class).findList(); + assertThat(list).hasSize(100000); + JsonContext jsonContext = DB.json(); + JsonFactory jsonFactory = new JsonFactory(); + for (int i = 0; i < 1; i++) { + long start = System.nanoTime(); + try (OutputStream out = new GZIPOutputStream(new FileOutputStream("temp.json.gz")); + JsonGenerator gen = jsonFactory.createGenerator(out, JsonEncoding.UTF8)) { + jsonContext.toJson(list, gen); + start = System.nanoTime() - start; + System.out.println("Serializing " + 100_000_000_000_000L / start + " Entities/s"); + } + } + + ObjectMapper mapper = new ObjectMapper(); + for (int i = 0; i < 10; i++) { + long start = System.nanoTime(); + try (InputStream in = new GZIPInputStream(new FileInputStream("temp.json.gz")); + JsonParser parser = mapper.createParser(in)) { + parser.nextToken(); + Vehicle v; + List<Vehicle> batch = new ArrayList<>(); + List<Object> batchId = new ArrayList<>(); + while ((v = read(parser)) != null) { + batchId.add(DB.beanId(v)); + batch.add(v); + if (batch.size() == 100) { + Map<Object, Vehicle> vDb = DB.find(Vehicle.class).where().idIn(batchId).findMap(); + for (Vehicle vehicle : batch) { + Vehicle db = vDb.get(vehicle.getId()); + DB.getDefault().pluginApi().mergeBeans(vehicle, db, null); + } + DB.saveAll(vDb.values()); + batch.clear(); + batchId.clear(); + } + } + //jsonContext.toList(Vehicle.class, parser); + start = System.nanoTime() - start; + System.out.println("DeSerializing " + 100_000_000_000_000L / start + " Entities/s"); + } + } + } + + private Vehicle read(JsonParser parser) throws IOException { + JsonToken token = parser.currentToken(); + if (token == null || token == JsonToken.START_ARRAY || token == JsonToken.END_OBJECT) { + // first invocation + token = parser.nextToken(); + } + if (token == JsonToken.START_OBJECT) { + Vehicle ret = DB.json().toBean(Vehicle.class, parser); + return ret; + } + return null; + } + private void setupData() { DB.createUpdate(CarAccessory.class, "delete from CarAccessory").execute(); DB.createUpdate(CarFuse.class, "delete from CarFuse").execute(); diff --git a/ebean-test/src/test/resources/ebean.properties b/ebean-test/src/test/resources/ebean.properties index 9dec823eed..e4eefd3c51 100644 --- a/ebean-test/src/test/resources/ebean.properties +++ b/ebean-test/src/test/resources/ebean.properties @@ -196,6 +196,7 @@ datasource.hana.username=EBEAN_TEST datasource.hana.password=Eb3an_test datasource.hana.url=jdbc:sap://hxehost:39013/?databaseName=HXE #datasource.hana.driver=com.sap.db.jdbc.Driver +# # parameters for migration test datasource.migrationtest.username=SA @@ -207,6 +208,21 @@ ebean.migrationtest.ddl.run=false ebean.migrationtest.ddl.header=-- Migrationscripts for ebean unittest ebean.migrationtest.migration.appName=migrationtest ebean.migrationtest.migration.migrationPath=migrationtest/dbmigration +ebean.migrationtest.migration.migrationInitPath=migrationtest/dbinit +ebean.migrationtest.migration.strict=true +ebean.migrationtest.migration.generate=true +ebean.migrationtest.migration.run=false +ebean.migrationtest.migration.includeIndex=true +ebean.migrationtest.migration.generateInit=true +ebean.migrationtest.migration.generatePendingDrop=auto +ebean.migrationtest.migration.platforms=db2luw,h2,hsqldb,mysql,mysql55,mariadb,postgres,oracle,sqlite,sqlserver17,hana,yugabyte +#migration.migrationtest.db2luw.prefix=db2 +#migration.migrationtest.sqlserver17.prefix=sqlserver +dbmigration.platform.mariadb.useMigrationStoredProcedures=true +dbmigration.platform.mysql.useMigrationStoredProcedures=true + + + ebean.migrationtest.migration.strict=true # enable stored procedures f dbmigration.platform.mariadb.useMigrationStoredProcedures=true @@ -222,4 +238,17 @@ ebean.migrationtest-history.ddl.run=false ebean.migrationtest-history.ddl.header=-- Migrationscripts for ebean unittest DbMigrationDropHistoryTest ebean.migrationtest-history.migration.appName=migrationtest-history ebean.migrationtest-history.migration.migrationPath=migrationtest-history/dbmigration +ebean.migrationtest-history.migration.migrationInitPath=migrationtest-history/dbinit ebean.migrationtest-history.migration.strict=true + +# ServerStartTest - can we run the migrations and do we find the correct ones! +datasource.db2-migration.username=unit +datasource.db2-migration.password=test +datasource.db2-migration.url=jdbc:db2://127.0.0.1:50000/unit +ebean.db2-migration.ddl.generate=false +ebean.db2-migration.ddl.run=false +ebean.db2-migration.migration.run=true +ebean.db2-migration.databasePlatformName=db2luw +# workaround for https://github.com/ebean-orm/ebean-migration/issues/102 +ebean.db2-migration.migration.migrationPath=migrationtest/dbmigration/db2 +ebean.db2-migration.migration.migrationInitPath=migrationtest/dbinit/db2 \ No newline at end of file diff --git a/ebean-test/src/test/resources/extra-ddl.xml b/ebean-test/src/test/resources/extra-ddl.xml index 130de7fac6..77113442d2 100644 --- a/ebean-test/src/test/resources/extra-ddl.xml +++ b/ebean-test/src/test/resources/extra-ddl.xml @@ -101,4 +101,5 @@ END IF; END $$ </ddl-script> + </extra-ddl> diff --git a/ebean-test/testconfig/ebean-oracle.properties b/ebean-test/testconfig/ebean-oracle.properties index 32b3af2491..24764943b2 100644 --- a/ebean-test/testconfig/ebean-oracle.properties +++ b/ebean-test/testconfig/ebean-oracle.properties @@ -1,3 +1,4 @@ ebean.test.platform=oracle ebean.test.dbName=test_eb +ebean.test.dbPassword=test datasource.default=oracle diff --git a/ebean-test/testplatforms.sh b/ebean-test/testplatforms.sh new file mode 100644 index 0000000000..1e19717c0e --- /dev/null +++ b/ebean-test/testplatforms.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# A small script, to run a certain test on all platforms +# invoke with ./testplatforms.sh -Dtest=DbMigrationTest +# Hint: in case of DbMigrationTest, you may disable ddl.run temporary + +# default H2 platform +set -e +mvn test "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-mysql.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-mariadb.properties "$@" +mvn surefire:test -Dprops.file=testconfig/ebean-mariadb-10.3.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-sqlserver17.properties "$@" +mvn surefire:test -Dprops.file=testconfig/ebean-sqlserver19.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-postgres.properties "$@" + +#mvn surefire:test -Dprops.file=testconfig/ebean-oracle.properties "$@" + +#mvn surefire:test -Dprops.file=testconfig/ebean-sqlite.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-hana.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-db2.properties "$@" + +## Test ignored +## mvn surefire:test -Dprops.file=testconfig/ebean-yugabyte.properties "$@" + +## Scripts are not correct +## mvn surefire:test -Dprops.file=testconfig/ebean-cockroach.properties "$@" + +## Transactions are not supported +## mvn surefire:test -Dprops.file=testconfig/ebean-clickhouse.properties "$@" + +## I cannot start nuodb +## mvn surefire:test -Dprops.file=testconfig/ebean-nuodb.properties.properties "$@" + + diff --git a/kotlin-querybean-generator/pom.xml b/kotlin-querybean-generator/pom.xml index b3d8c058b3..7ed9ebf92c 100644 --- a/kotlin-querybean-generator/pom.xml +++ b/kotlin-querybean-generator/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <name>kotlin querybean generator</name> @@ -29,7 +29,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-querybean</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> @@ -43,7 +43,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-core</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> @@ -64,14 +64,14 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-h2</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-ddl-generator</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <scope>test</scope> </dependency> diff --git a/platforms/all/pom.xml b/platforms/all/pom.xml index 2d1c4c2d0c..e1d46e141a 100644 --- a/platforms/all/pom.xml +++ b/platforms/all/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-all</artifactId> @@ -14,67 +14,67 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-h2</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-clickhouse</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-db2</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-hana</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-hsqldb</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-mysql</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-mariadb</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-nuodb</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-oracle</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-postgres</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-sqlanywhere</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-sqlite</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-sqlserver</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> </dependencies> diff --git a/platforms/clickhouse/pom.xml b/platforms/clickhouse/pom.xml index 1e59067a9c..5504b51392 100644 --- a/platforms/clickhouse/pom.xml +++ b/platforms/clickhouse/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-clickhouse</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> </dependencies> diff --git a/platforms/db2/pom.xml b/platforms/db2/pom.xml index 749e59200b..9901757d6a 100644 --- a/platforms/db2/pom.xml +++ b/platforms/db2/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-db2</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> </dependencies> diff --git a/platforms/h2/pom.xml b/platforms/h2/pom.xml index fc206ca7ec..98b9608b0e 100644 --- a/platforms/h2/pom.xml +++ b/platforms/h2/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-h2</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <!-- Provided scope so that the H2HistoryTrigger can live in Ebean core diff --git a/platforms/hana/pom.xml b/platforms/hana/pom.xml index c61edada7e..7996d13c26 100644 --- a/platforms/hana/pom.xml +++ b/platforms/hana/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-hana</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> diff --git a/platforms/hsqldb/pom.xml b/platforms/hsqldb/pom.xml index c3b8038ffb..c0bfef98d7 100644 --- a/platforms/hsqldb/pom.xml +++ b/platforms/hsqldb/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-hsqldb</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-h2</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> </dependencies> diff --git a/platforms/mariadb/pom.xml b/platforms/mariadb/pom.xml index 1521ffd233..e463f5a80a 100644 --- a/platforms/mariadb/pom.xml +++ b/platforms/mariadb/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-mariadb</artifactId> @@ -14,13 +14,13 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-platform-mysql</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> </dependencies> diff --git a/platforms/mysql/pom.xml b/platforms/mysql/pom.xml index f3aacae06e..a45cf751cc 100644 --- a/platforms/mysql/pom.xml +++ b/platforms/mysql/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-mysql</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> diff --git a/platforms/nuodb/pom.xml b/platforms/nuodb/pom.xml index 62e0cf1b00..ab281b2f7b 100644 --- a/platforms/nuodb/pom.xml +++ b/platforms/nuodb/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-nuodb</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> </dependencies> diff --git a/platforms/oracle/pom.xml b/platforms/oracle/pom.xml index cf16625362..0bfc56a13d 100644 --- a/platforms/oracle/pom.xml +++ b/platforms/oracle/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-oracle</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> diff --git a/platforms/pom.xml b/platforms/pom.xml index 3e500a385f..df191f97c2 100644 --- a/platforms/pom.xml +++ b/platforms/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>platforms</artifactId> diff --git a/platforms/postgres/pom.xml b/platforms/postgres/pom.xml index c50ca967fa..1bf84251b4 100644 --- a/platforms/postgres/pom.xml +++ b/platforms/postgres/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-postgres</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> diff --git a/platforms/sqlanywhere/pom.xml b/platforms/sqlanywhere/pom.xml index 40e30e109f..bf26e34adf 100644 --- a/platforms/sqlanywhere/pom.xml +++ b/platforms/sqlanywhere/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-sqlanywhere</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> </dependencies> diff --git a/platforms/sqlite/pom.xml b/platforms/sqlite/pom.xml index 782f681884..343c44df47 100644 --- a/platforms/sqlite/pom.xml +++ b/platforms/sqlite/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-sqlite</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> </dependencies> diff --git a/platforms/sqlserver/pom.xml b/platforms/sqlserver/pom.xml index 8dd75c0bde..c7c84987b1 100644 --- a/platforms/sqlserver/pom.xml +++ b/platforms/sqlserver/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>platforms</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>ebean-platform-sqlserver</artifactId> @@ -14,7 +14,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-api</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </dependency> <dependency> diff --git a/pom.xml b/pom.xml index e821cf0972..5a93793cf7 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ <groupId>io.ebean</groupId> <artifactId>ebean-parent</artifactId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> <packaging>pom</packaging> <name>ebean parent</name> @@ -17,8 +17,8 @@ <url>https://ebean.io/</url> <scm> - <developerConnection>scm:git:git@github.com:ebean-orm/ebean.git</developerConnection> - <tag>HEAD</tag> + <developerConnection>scm:git:git@github.com:FOCONIS/ebean.git</developerConnection> + <tag>ebean-parent-13.6.4-FOC1</tag> </scm> <licenses> @@ -52,11 +52,24 @@ <ebean-migration.version>13.7.0</ebean-migration.version> <ebean-test-containers.version>6.3</ebean-test-containers.version> <ebean-datasource.version>8.5</ebean-datasource.version> - <ebean-agent.version>13.17.3</ebean-agent.version> - <ebean-maven-plugin.version>13.17.3</ebean-maven-plugin.version> + <ebean-agent.version>13.13.1-FOC2</ebean-agent.version> + <ebean-maven-plugin.version>13.17.2</ebean-maven-plugin.version> <surefire.useModulePath>false</surefire.useModulePath> </properties> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + <version>1.6</version> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + </plugins> + </build> + <dependencies> <dependency> @@ -102,6 +115,66 @@ </modules> <profiles> + <profile> + <id>foconis</id> + <activation> + <activeByDefault>true</activeByDefault> + </activation> + <distributionManagement> + <repository> + <id>foconis-release</id> + <name>FOCONIS Release Repository</name> + <url>https://mvnrepo.foconis.de/repository/release/</url> + </repository> + <snapshotRepository> + <id>foconis-snapshot</id> + <name>FOCONIS Snapshot Repository</name> + <url>https://mvnrepo.foconis.de/repository/snapshot/</url> + </snapshotRepository> + </distributionManagement> + <repositories> + <repository> + <id>foconis-release</id> + <url>https://mvnrepo.foconis.de/repository/release/</url> + </repository> + <repository> + <id>foconis-snapshot</id> + <url>https://mvnrepo.foconis.de/repository/snapshot/</url> + </repository> + </repositories> + <pluginRepositories> + <pluginRepository> + <id>foconis-release</id> + <url>https://mvnrepo.foconis.de/repository/release/</url> + </pluginRepository> + <pluginRepository> + <id>foconis-snapshot</id> + <url>https://mvnrepo.foconis.de/repository/snapshot/</url> + </pluginRepository> + </pluginRepositories> + </profile> + <profile> + <id>github</id> + <distributionManagement> + <repository> + <id>github-release</id> + <name>FOCONIS Github Release Repository</name> + <url>https://maven.pkg.github.com/foconis/ebean</url> + </repository> + </distributionManagement> + <repositories> + <repository> + <id>github-release</id> + <url>https://maven.pkg.github.com/foconis/ebean-agent</url> + </repository> + </repositories> + <pluginRepositories> + <pluginRepository> + <id>github-release</id> + <url>https://maven.pkg.github.com/foconis/ebean-agent</url> + </pluginRepository> + </pluginRepositories> + </profile> <profile> <id>central</id> </profile> diff --git a/querybean-generator/pom.xml b/querybean-generator/pom.xml index 1008bd9fb0..1b18ba4d73 100644 --- a/querybean-generator/pom.xml +++ b/querybean-generator/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <name>querybean generator</name> diff --git a/release.md b/release.md new file mode 100644 index 0000000000..0083b5ae3e --- /dev/null +++ b/release.md @@ -0,0 +1,75 @@ +## Merge back Robs master: + + +```bash +# First sync version to non-foc version. Version must be equal with rob's branch: +mvn versions:set -DgenerateBackupPoms=false -DnewVersion=13.x.x-SNAPSHOT +git commit -am "Sync version to upstream" + +# add remote (one time step) +git remote add upstream git@github.com:ebean-orm/ebean.git + +git fetch upstream +git merge upstream/master +``` +Now resolve all merge conflicts + +```bash +# Set back to foc-version +mvn versions:set -DgenerateBackupPoms=false -DnewVersion=13.x.x-FOCx-SNAPSHOT +``` + +Then check, if all -SNAPSHOT versions are foc-version + + +## Release command + + +We @foconis use this command to release. + +```bash +mvn release:prepare release:perform -Darguments="-Dgpg.skip -DskipTests" -Pfoconis +``` + +### Build releases for github and/or jakarta + +First, checkout latest release commit with +```bash +git checkout HEAD~1 +``` + +Build github release: + +```bash +mvn clean source:jar deploy -DskipTests -Pgithub -T 8 +``` + +Switch to Jakarta: + +```bash +export EBEAN_VERSION=$(grep "<version>13" pom.xml | awk -F '[<>]' '{print $3}') +# first, set to snapshot, because of kotlin +mvn versions:set -DgenerateBackupPoms=false -DnewVersion=${EBEAN_VERSION}-SNAPSHOT -Pjdk16plus -Pjdk15less +mvn versions:set -DgenerateBackupPoms=false -DnewVersion=${EBEAN_VERSION}-jakarta -Pjdk16plus -Pjdk15less +./javax-to-jakarta.sh +mvn clean source:jar deploy -DskipTests -Pfoconis -T 8 +mvn clean source:jar deploy -DskipTests -Pgithub -T 8 +# do not commit, switch back to master +git switch -f master +``` + +## Fix POMs after release + +After a release, you may have to fix poms with + +```bash +mvn versions:update-parent -DallowSnapshots=true -DgenerateBackupPoms=false -Pjdk16plus -Pjdk15less +``` + + # wenn es Probleme mit Versionen gibt, dann manuell ebean-kotlin/pom.xml, tests/test-java16/pom.xml und tests/test-kotlin/pom.xml, usw. anpassen + # wenn es bei kotlin-querybean-generator krachts, dann den Modul auskommentieren oder Modul auslassen und mit ... -rf :NÄCHSTE-MODUL weitermachen + +## generate Java classes from .xsd: + + export JAVA_TOOL_OPTIONS="-Duser.language=en -Duser.country=US -Dfile.encoding=UTF-8" + /c/Program\ Files/Java/jdk1.8.0_201/bin/xjc.exe src/main/resources/ebean-dbmigration-1.0.xsd -d src/main/java -p io.ebeaninternal.dbmigration.migration diff --git a/tests/pom.xml b/tests/pom.xml index 829041eeac..76a7df3215 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>ebean-parent</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>tests</artifactId> diff --git a/tests/test-java16/pom.xml b/tests/test-java16/pom.xml index da52ffce6c..df680fa2f5 100644 --- a/tests/test-java16/pom.xml +++ b/tests/test-java16/pom.xml @@ -5,7 +5,7 @@ <parent> <artifactId>tests</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>test-java16</artifactId> @@ -18,7 +18,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean</artifactId> - <version>13.17.3</version> + <version>${project.version}</version> </dependency> <dependency> @@ -30,7 +30,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-test</artifactId> - <version>13.17.3</version> + <version>${project.version}</version> <scope>test</scope> </dependency> @@ -70,6 +70,13 @@ </configuration> </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> </plugins> </build> diff --git a/tests/test-kotlin/pom.xml b/tests/test-kotlin/pom.xml index b94173fcd1..9bc6760ef3 100644 --- a/tests/test-kotlin/pom.xml +++ b/tests/test-kotlin/pom.xml @@ -6,7 +6,7 @@ <parent> <artifactId>tests</artifactId> <groupId>io.ebean</groupId> - <version>13.17.3</version> + <version>13.17.3-FOC12-SNAPSHOT</version> </parent> <artifactId>test-kotlin</artifactId> @@ -40,7 +40,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean</artifactId> - <version>13.17.3</version> + <version>${project.version}</version> </dependency> <dependency> @@ -53,7 +53,7 @@ <dependency> <groupId>io.ebean</groupId> <artifactId>ebean-test</artifactId> - <version>13.17.3</version> + <version>${project.version}</version> <scope>test</scope> </dependency> @@ -70,6 +70,11 @@ <sourceDirectory>src/main/kotlin</sourceDirectory> <testSourceDirectory>src/test/kotlin</testSourceDirectory> <plugins> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <extensions>false</extensions> + </plugin> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId>