Compare commits

..
Author SHA1 Message Date
Robin Bygrave 436988694d [maven-release-plugin] prepare release avaje-ebeanorm-7.2.2 2016-03-22 09:46:58 +13:00
Robin Bygrave 90f786babd #610 - ManyToOne returns wrong ID value (prior ManyToOne with inheritance and null value). Regression introduced in 7.2.1. 2016-03-22 09:46:12 +13:00
Robin Bygrave 0d8146d453 [maven-release-plugin] prepare for next development iteration 2016-03-21 12:20:17 +13:00
Robin Bygrave 0eca613b41 [maven-release-plugin] prepare release avaje-ebeanorm-7.2.1 2016-03-21 12:19:57 +13:00
Robin Bygrave dcde54fca4 #100 - SQLITE (365) : ADD CONSTRAINT DDL SQLite Incompatibility 2016-03-21 11:06:52 +13:00
Robin Bygrave 49b8efbc30 #609 - SQLite DDL - disable use of comments and alter table add foreign key (as these are not supported by SQLite) 2016-03-21 10:31:57 +13:00
Robin Bygrave fc70161909 No effective change - bump SQLite jdbc driver for testing 2016-03-21 10:31:42 +13:00
Rob Bygrave 44ba6d3bb2 Merge pull request #608 from btimo/fixSQLiteDdl
SQLite Ddl : autoincrement attribute
2016-03-21 10:14:05 +13:00
Robin Bygrave d56e071f53 No effective change - javadoc only 2016-03-21 09:20:11 +13:00
Robin Bygrave ce33c230c7 No effective change - javadoc only 2016-03-21 08:43:33 +13:00
btimo 2da6a6960e - fix SQLiteDdl : https://www.sqlite.org/autoinc.html 2016-03-18 17:55:29 +01:00
Robin Bygrave 62f88c9e4f #607 - bool conversion for and(), or() and not() 2016-03-18 16:19:41 +13:00
Robin Bygrave 42f65ead47 #607 - must(), should(), mustNot() always mark setUseDocStore(true); 2016-03-18 16:17:32 +13:00
Robin Bygrave 8e98e88e8a #607 - Tidy "text" extensions to criteria API - remove endMust(), endShould(), endMustNot() ... just use endJunction() 2016-03-18 14:24:07 +13:00
Robin Bygrave 5de67e1939 #606 - Add not() as a junction expression (like and() and or()) to the query criteria API 2016-03-18 14:11:51 +13:00
Robin Bygrave 3ddd7099b5 #605 - Add and() and or() ... as synonyms for conjunction() and disjunction() ... to the "Query Criteria API" 2016-03-18 13:06:10 +13:00
Robin Bygrave 5d889790c2 No effective change - move related methods together 2016-03-18 12:53:15 +13:00
Robin Bygrave 636acc61e5 #594 ElasticSearch - Add match / TextExpression support - move expressions onto ExpressionList (remove TextExpressionList) 2016-03-18 12:51:39 +13:00
Robin Bygrave cb347ff5fa #602 - Elastic - Add support for ebean.docstore.generateMapping 2016-03-15 17:10:03 +13:00
Robin Bygrave db9d39ef61 #583 - MSSQL Server - Delete query syntax error, mssql does not like the table alias in the delete statement 2016-03-15 01:03:08 +13:00
Robin Bygrave 5216014d50 #601 - Second Nested filterMany not executed but joined 2016-03-14 23:54:34 +13:00
Robin Bygrave c40715733e #600 - Unique index or primary key violation - double insert attempted on ManyToMany intersection 2016-03-14 22:32:03 +13:00
Robin Bygrave 9bcc8a77fe No effective change - imports 2016-03-14 21:28:14 +13:00
Robin Bygrave 7be3a01823 No effective change - tidy up tests 2016-03-14 21:06:48 +13:00
Robin Bygrave 0ca4f92b26 #592 - Fetched empty collection of null child of parent entity 2016-03-14 20:10:07 +13:00
Robin Bygrave b99f002968 #599 - ElasticSearch - add query join support for find, findList, findPagedList, findEach, findEachWhile 2016-03-14 17:40:26 +13:00
Robin Bygrave fa451078b7 No effective change - cleanup imports 2016-03-11 21:56:11 +13:00
Robin Bygrave 49121a9c1f #595 - Refactor - persistence context root type handling - fully expect root type 2016-03-11 20:54:57 +13:00
Robin Bygrave 33f69902dc #595 - Refactor - persistence context root type handling - contextGet() and contextRef() 2016-03-11 20:26:03 +13:00
Robin Bygrave cf783fbbb7 #595 - Refactor - persistence context root type handling - AssocOne split simple and inherit 2016-03-11 19:40:02 +13:00
Robin Bygrave cf62ea4c2f #595 - Refactor - persistence context root type handling 2016-03-11 17:34:45 +13:00
Robin Bygrave 049bf76cc4 #594 - ElasticSearch - Add support for TextSimple, TextQueryString and TextCommon full text search expressions 2016-03-11 14:52:27 +13:00
Robin Bygrave 75a84af3f8 #594 - ElasticSearch - Use AbstractTextExpression 2016-03-10 11:30:42 +13:00
Robin Bygrave 93680f5d19 Bump agent to 4.9.2 2016-03-10 11:29:51 +13:00
Robin Bygrave faf7df1c69 #594 - ElasticSearch - multi match support 2016-03-09 23:44:23 +13:00
Robin Bygrave 34dcc98908 #594 - ElasticSearch - Add match / TextExpression support (Full text search expression) 2016-03-09 21:48:22 +13:00
Robin Bygrave 3c63b79324 #589 - ElasticSearch - Improve update notification when not mapped but has nested invalidation 2016-03-08 22:14:23 +13:00
Robin Bygrave 2d93d592c1 #593 - Refactor SPI - Move methods from ElPropertyValue to ExpressionPath 2016-03-08 21:34:27 +13:00
Robin Bygrave 57836af5b5 #593 - Refactor SPI - Move methods from ElPropertyValue to ExpressionPath 2016-03-08 21:21:46 +13:00
Robin Bygrave ba871d6684 #593 - Refactor SPI - Move methods from ElPropertyValue to ExpressionPath 2016-03-08 21:09:49 +13:00
Robin Bygrave 583fc11945 #593 - Refactor SPI - Move methods from ElPropertyValue to ExpressionPath 2016-03-08 21:02:39 +13:00
Robin Bygrave 3ba4660bbb No effective change - format code 2016-03-08 18:33:32 +13:00
Robin Bygrave 37b86f2191 #589 - ElasticSearch - refactor to DocQueryContext 2016-03-08 00:00:02 +13:00
Robin Bygrave da8766533f #589 - ElasticSearch - extend mapping options 2016-03-07 22:00:41 +13:00
Robin Bygrave ae31117b8f #590 - Refactor internals - Remove duplicate isBatch() logic in JdbcTransaction 2016-03-07 19:23:50 +13:00
Robin Bygrave cf825f7ac4 #589 - ElasticSearch, Transaction - rename DocStoreMode and batch size methods 2016-03-07 19:15:08 +13:00
Robin Bygrave a0609b1726 #589 - ElasticSearch - DocStoreMode renamed from DocStoreEvent 2016-03-07 19:10:15 +13:00
Robin Bygrave 32bcd5e853 #589 - ElasticSearch - JsonReader given BeanType 2016-03-07 16:27:16 +13:00
Robin Bygrave f082fce1ff Bump pom to 7.2.1-SNAPSHOT 2016-03-07 16:26:18 +13:00
Robin Bygrave 7fc58a1842 #589 - ElasticSearch - DocStoreConfig ... mapping path, suffix, resources 2016-03-07 14:21:09 +13:00
Robin Bygrave f2ffbb2c5b #589 - ElasticSearch - Add copyIndex() via Query, Add findEachWhile() 2016-03-07 13:49:23 +13:00
Robin Bygrave 5c0fb7c8cd #584 - ElasticSearch: Internal changes - Enum and UUID tests 2016-03-04 00:04:58 +13:00
Robin Bygrave 04c9db1e42 #586 - Refactor the UUID ScalarTypes (Native, Varchar and Binary) to have a common base 2016-03-04 00:00:05 +13:00
Robin Bygrave 8f7de950e2 No effective change - reformat, early check for UUID type 2016-03-03 23:56:30 +13:00
Robin Bygrave 53dd810b9a #587 - Refactor - Change JsonContext.getScalar() to JsonContext.writeScalar() 2016-03-03 23:38:09 +13:00
Robin Bygrave 290ca6ba90 #586 - Refactor the UUID ScalarTypes (Native, Varchar and Binary) to have a common base 2016-03-03 22:10:28 +13:00
Robin Bygrave 992f5797a7 #586 - Refactor the UUID ScalarTypes (Native, Varchar and Binary) to have a common base 2016-03-03 22:10:04 +13:00
Robin Bygrave 798976fbd2 #585 - Refactor of ScalarType jsonWrite() method such that it only takes JsonGenerator - part 4, tidy up 2016-03-03 21:39:10 +13:00
Robin Bygrave c315067ff0 #585 - Refactor of ScalarType jsonWrite() method such that it only takes JsonGenerator - part 3, remove JsonToken from jsonRead() 2016-03-03 21:06:09 +13:00
Robin Bygrave a4f0e3936a #585 - Refactor of ScalarType jsonWrite() method such that it only takes JsonGenerator - part 2, use JsonGenerator 2016-03-03 20:27:13 +13:00
Robin Bygrave fc96685ed0 #585 - Refactor of ScalarType jsonWrite() method such that it only takes JsonGenerator - part 1, remove field name 2016-03-03 19:51:07 +13:00
Robin Bygrave 5b10d06348 #584 - ElasticSearch: Internal changes for config, expressions and JSON conversion 2016-03-03 17:46:32 +13:00
Robin Bygrave ca73eff519 #582 - ElasticSearch: Add setUseDocStore(true) on ExpressionList ... for fluid query style 2016-03-02 23:25:58 +13:00
Robin Bygrave 646de50ae8 #581 - ElasticSearch - DocStoreConfig, read dropCreate not read from properties file 2016-03-02 23:22:13 +13:00
Robin Bygrave e2558a1e24 #578 - Deprecated Joda Time API - LocalDate.toDateMidnight() usage causes exception to be thrown, change to LocalDate.toDateTimeAtStartOfDay() 2016-03-02 08:12:53 +13:00
Robin Bygrave ad596a2043 #580 - @Enumerated(EnumType.STRING) on Enum uses toString() rather than name() 2016-03-01 17:35:55 +13:00
Robin Bygrave 0a60cd4e58 [maven-release-plugin] prepare for next development iteration 2016-03-01 16:17:14 +13:00
Robin Bygrave ec38137e5c [maven-release-plugin] prepare release avaje-ebeanorm-7.1.1 2016-03-01 16:16:53 +13:00
Robin Bygrave bfcbd335c8 Reduce logging via logback-test.xml for CI etc 2016-03-01 16:14:31 +13:00
Robin Bygrave 2cafdf709d bump pom to 7.1.1-SNAPSHOT 2016-03-01 16:12:40 +13:00
Robin Bygrave ed2ea13deb #578 - Deprecated Joda Time API - LocalDate.toDateMidnight() usage causes exception to be thrown, change to LocalDate.toDateTimeAtStartOfDay() 2016-03-01 16:09:57 +13:00
Robin Bygrave 8d3b4a0407 #304 - ElasticSearch integration part 1 / doc store integration 2016-03-01 15:36:44 +13:00
Robin Bygrave cb1fe297d6 #579 - Postgres History DDL - Revert of #544 such that Postgres @History DB trigger uses current_timestamp (transaction start time) 2016-03-01 13:15:19 +13:00
Robin Bygrave 6561c118ec #575 - Refactor PathProperties, extract FetchPath interface, rename get(path) -> getProperties(path) 2016-02-29 10:22:59 +13:00
Robin Bygrave eda7fe24c1 #576 - DateTimeJsonParser error when date time has no millisecond precision -java.text.ParseException: Unparseable date: "2016-02-28T20:39:00Z" 2016-02-29 10:14:17 +13:00
Robin Bygrave 2353049c6f #574 - Refactor - rename .plugin objects; SpiBeanType -> BeanType and SpiServerPlugin -> Plugin 2016-02-29 09:19:44 +13:00
Robin Bygrave 6c49ac0857 [maven-release-plugin] prepare for next development iteration 2016-02-17 21:50:48 +13:00
Robin Bygrave 76033385dc [maven-release-plugin] prepare release avaje-ebeanorm-6.18.1 2016-02-17 21:50:22 +13:00
Robin Bygrave b61cd30e29 Bump pom to 6.18.1-SNAPSHOT 2016-02-17 21:49:07 +13:00
Robin Bygrave 18edaf877b #420 - Review JodaLocalTime (being stored and fetched using UTC) 2016-02-17 21:36:47 +13:00
Robin Bygrave f055a0d0cb #420 - Review JodaLocalTime (being stored and fetched using UTC) 2016-02-17 21:34:52 +13:00
Robin Bygrave a22574b0c9 #552 - wrong DDL generation with complex columnDefinition 2016-02-17 20:34:13 +13:00
Robin Bygrave 3130f751f5 Merge branch 'refactor-query-hash' 2016-02-17 19:45:01 +13:00
Robin Bygrave c83606c1da #566 - Refactor internals - SpiQuerySecondary and DLoadContext 2016-02-17 19:44:30 +13:00
Robin Bygrave 439858b259 #566 - Refactor internals -OrmQueryDetailProperties - remove hasProperties() 2016-02-17 16:53:32 +13:00
Robin Bygrave 3d3948aa8b #566 - Refactor internals - OrmQueryDetail, skip fetch path sort when empty 2016-02-17 16:47:38 +13:00
Robin Bygrave 27045baf44 #566 - Refactor internals - OrmQueryDetailProperties with final FetchConfig 2016-02-17 16:33:38 +13:00
Robin Bygrave 5c6d6608ef #566 - Refactor internals - Initial tidy on OrmQueryDetail and OrmQueryDetailProperties - internals for fetch paths etc 2016-02-17 13:59:48 +13:00
Robin Bygrave 2fcaab44c6 #566 - Refactor internals - Initial tidy on OrmQueryDetail and OrmQueryDetailProperties 2016-02-17 10:52:55 +13:00
Robin Bygrave 69bf1758bc #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - tidy up OrmQueryRequest 2016-02-16 17:07:56 +13:00
Robin Bygrave af554410fd #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - tidy up OrmQueryDetail 2016-02-16 16:10:39 +13:00
Robin Bygrave ae7827c483 #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - tidy up createQueryRequest - part 2 2016-02-16 09:21:10 +13:00
Robin Bygrave 4df2d4020e #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - tidy up createQueryRequest 2016-02-16 08:57:12 +13:00
Robin Bygrave 1aeffd31e0 #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - no effective change, reorder methods 2016-02-16 01:46:12 +13:00
Robin Bygrave 5c59066ff8 #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - rawSql key 2016-02-16 01:06:30 +13:00
Robin Bygrave a2bf8a0e46 [maven-release-plugin] prepare for next development iteration 2016-02-15 23:49:34 +13:00
Robin Bygrave 3ef77ea410 [maven-release-plugin] prepare release avaje-ebeanorm-6.17.3 2016-02-15 23:49:08 +13:00
Robin Bygrave fe2c2e5331 #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - sameByPlan() & sameByBind() 2016-02-15 23:46:28 +13:00
Robin Bygrave 3d91a4f927 #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - sameByPlan() & sameByBind() 2016-02-15 23:44:56 +13:00
Robin Bygrave 165d4aca92 #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - remove unused 2016-02-15 10:21:22 +13:00
Robin Bygrave 076f8a03b9 #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - move to protected calculateQueryPlanHash() 2016-02-13 21:22:52 +13:00
Robin Bygrave ca944eb756 #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - refactor InQuery and Exists query 2016-02-13 20:52:41 +13:00
Robin Bygrave 769e4bb488 #566 - Refactor internals - SpiExpression, add prepareExpression() to merge queryAutoTuneHash() and queryPlanHash() - initial 2016-02-13 20:13:16 +13:00
Robin Bygrave 0d456b2ad6 Update TestOrderByFetch for running in Postgres ... by adding a 'name' property to the beans so that there is at least one property in the inserts 2016-02-13 13:09:41 +13:00
Robin Bygrave 39e913cbe2 #565 - Query plan hash collision on firstRow/maxRows combinations - change prime 2016-02-13 12:30:31 +13:00
Robin Bygrave ae38672e02 #565 - Query plan hash collision on firstRow/maxRows combinations - tests 2016-02-13 12:30:00 +13:00
Robin Bygrave 8a8b36ec1e #462 - When fetching, collections can get populated with too many entities - Fix 2016-02-13 00:43:47 +13:00
Robin Bygrave 62aca1e77b #462 - When fetching, collections can get populated with too many entities - Fix 2016-02-12 23:33:17 +13:00
Robin Bygrave f0b80068b6 #462 - No effective change, reformat code ... When fetching, collections can get populated with too many entities - 2016-02-12 23:32:43 +13:00
Rob Bygrave 5721fe3807 Merge pull request #560 from nedap/master
Add test for broken fetching issue #462
2016-02-12 21:58:35 +13:00
Robin Bygrave afffb88a29 [maven-release-plugin] prepare for next development iteration 2016-02-12 21:51:59 +13:00
Robin Bygrave 4c3a125192 [maven-release-plugin] prepare release avaje-ebeanorm-6.17.2 2016-02-12 21:51:37 +13:00
Robin Bygrave f8804f8d18 #564 - DDL DB Migration - migration generation on history table, pending drop columns missing in the history view and tigger 2016-02-12 21:28:19 +13:00
Robin Bygrave 4848c24a0a #563 - ENH: DDL DB Migration - add a suppressDropsForever="true" attribute such that drops we never want to run are not warned on and can't be selected for a migration. 2016-02-12 14:33:19 +13:00
Robin Bygrave 4c3d1d9edd #561 - DDL DB Migration - change "ddl.migration.pendingDrop" to "ddl.migration.pendingDropsFor" 2016-02-12 11:04:25 +13:00
Pieter Bos 42b112c3d7 Add test for broken fetching issue #462 2016-02-11 17:00:39 +01:00
Robin Bygrave 59996da07a [maven-release-plugin] prepare for next development iteration 2016-02-11 22:43:53 +13:00
Robin Bygrave 32d4b83a86 [maven-release-plugin] prepare release avaje-ebeanorm-6.17.1 2016-02-11 22:43:22 +13:00
Robin Bygrave ca824c5e0d Refactor / fix .gitignore of test resources 2016-02-11 22:33:25 +13:00
Robin Bygrave b2f7664316 Bump pom to 6.17.1-SNAPSHOT 2016-02-11 22:20:17 +13:00
Robin Bygrave 062f7a281f #559 - DDL - DB Migration refactor - remove rollback.ddl generation (ultimately not used by FlywayDb etc) 2016-02-11 22:17:35 +13:00
Robin Bygrave 019ce4e614 #558 - Internal refactor - MTable pending dropped columns 2016-02-11 20:43:45 +13:00
Robin Bygrave 6b490ed8b1 #557 - DDL - DB Migration refactor - remove drop.ddl and use pendingDrops. 2016-02-11 20:26:53 +13:00
Robin Bygrave 1bee6fcae5 #557 - DDL - DB Migration refactor - remove drop.ddl and use pendingDrops. 2016-02-11 20:15:49 +13:00
Robin Bygrave 097552b8d3 [maven-release-plugin] prepare for next development iteration 2016-02-10 20:53:52 +13:00
Robin Bygrave 9fbfca5f73 [maven-release-plugin] prepare release avaje-ebeanorm-6.16.5 2016-02-10 20:53:29 +13:00
Robin Bygrave 9e70ba915f #556 - DDL - DB Migration for History - change treatment of dropped columns such that they are still included in apply script 2016-02-10 16:05:59 +13:00
Robin Bygrave a1a5dcd035 #556 - DDL - DB Migration for History - change treatment of dropped columns such that they are still included in apply script 2016-02-10 16:04:46 +13:00
Robin Bygrave d2b048b0b4 No effective change - reformat code 2016-02-09 14:51:26 +13:00
Robin Bygrave 4272c17689 #555 - Use a common timestamp value for @WhenCreated and @WhenModified 2016-02-09 14:48:41 +13:00
Robin Bygrave 11723e3a32 #554 - DDL - DB Migration with @History and Postgres - views need to be dropped early to support migration changes 2016-02-05 11:54:52 +13:00
Robin Bygrave 237cdcdd3d #554 - DDL - DB Migration with @History and Postgres - views need to be dropped early to support migration changes 2016-02-05 11:52:06 +13:00
Robin Bygrave e96bee74ae [maven-release-plugin] prepare for next development iteration 2016-02-05 00:15:30 +13:00
Robin Bygrave c8b57c3302 [maven-release-plugin] prepare release avaje-ebeanorm-6.16.4 2016-02-05 00:15:04 +13:00
Robin Bygrave f65ea96b89 #553 - DDL - DB Migration with @History and Postgres needs to re-create the "with_history" view when columns dropped 2016-02-05 00:11:25 +13:00
Robin Bygrave a1bd0b469a #551 - DDL - DB Migration with @History and @Draftable - @DraftOnly columns added when not part of change 2016-02-04 22:31:07 +13:00
Robin Bygrave fd5563e3da No effective change - javadoc update for RawSql tableAliasMapping 2016-02-02 15:35:31 +13:00
Robin Bygrave f9156ede55 No effective change - add tests for RawSql tableAlias mapping of root type to null path and not mapping it 2016-02-02 14:58:59 +13:00
Robin Bygrave 10d2d591db [maven-release-plugin] prepare for next development iteration 2016-02-02 13:38:09 +13:00
Robin Bygrave 998d8ed6f2 [maven-release-plugin] prepare release avaje-ebeanorm-6.16.3 2016-02-02 13:37:46 +13:00
Robin Bygrave a8da472872 #550 - ENH: Add generated file warning to DB migration XML files 2016-02-02 13:33:46 +13:00
Robin Bygrave a9cdf9736a Bump maven plugin version to 4.9.1 2016-02-02 13:23:46 +13:00
Robin Bygrave c930a1dd6b #549 - Regression: ArrayIndexOutOfBoundsException when lazy loading on bean with inheritance - tidy up 2016-02-02 11:29:38 +13:00
Robin Bygrave 67ce6ef58e #549 - Regression: ArrayIndexOutOfBoundsException when lazy loading on bean with inheritance 2016-02-02 11:24:46 +13:00
Robin Bygrave c4f28efc45 Testing CI - .travis - git depth + skip mvn install 2016-01-28 10:54:16 +13:00
Robin Bygrave afa4bc06d9 Testing CI - .travis - try setup_database 2016-01-28 10:44:14 +13:00
Robin Bygrave 828c9ce128 Testing CI - .travis - try sudo required 2016-01-28 09:31:18 +13:00
Robin Bygrave 6f37a0bd72 Testing CI - .travis - print surefire reports + setup_database script 2016-01-28 09:20:27 +13:00
Robin Bygrave 036086af02 Testing CI - .travis - print surefire reports on failure 2016-01-28 09:00:44 +13:00
Robin Bygrave f34b5e52e0 [maven-release-plugin] prepare for next development iteration 2016-01-28 08:36:40 +13:00
500 changed files with 21731 additions and 6117 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
*.autofetch
*.sql
*create-all.sql
*drop-all.sql
*.orig
.classpath
.project
+13 -4
View File
@@ -1,7 +1,11 @@
sudo: required
language: java
jdk:
- oraclejdk8
git:
depth: 3
addons:
postgresql: "9.4"
@@ -9,15 +13,20 @@ services:
- postgresql
before_script:
- psql -c 'create extension pgcrypto;' -U postgres
- psql -c 'create extension hstore;' -U postgres
- psql -c 'create extension postgis;' -U postgres
- psql -c 'create database unit;' -U postgres
- ./.travis/setup_database
env:
- EBEAN_DB=h2
- EBEAN_DB=pg
install: true
script:
- mvn clean test
after_failure:
- ./.travis/print_surefire_reports
cache:
directories:
- $HOME/.m2
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
echo "\n=== SUREFIRE REPORTS ===\n"
for file in target/surefire-reports/*.txt
do
echo ${file}
cat ${file}
echo
done
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
echo "\n=== SETUP DATABASE ===\n"
#set -e
#set -x
# Setup postgres' users and databases
sudo -u postgres psql -c "CREATE USER unit WITH PASSWORD 'unit';"
sudo -u postgres psql -c 'CREATE DATABASE unit WITH OWNER unit;'
sudo -u postgres psql unit -c 'CREATE EXTENSION hstore;'
sudo -u postgres psql unit -c 'CREATE EXTENSION pgcrypto;'
#sudo -u postgres psql test2 -c 'CREATE EXTENSION postgis;'
+8 -8
View File
@@ -9,7 +9,7 @@
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm</artifactId>
<version>6.16.2</version>
<version>7.2.2</version>
<packaging>jar</packaging>
<name>avaje-ebeanorm</name>
@@ -73,7 +73,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.3</version>
<version>2.6.5</version>
<scope>provided</scope>
</dependency>
@@ -81,7 +81,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.5.3</version>
<version>2.6.5</version>
</dependency>
<dependency>
@@ -138,8 +138,8 @@
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
<version>2.1.0</version>
<scope>test</scope>
<version>2.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -152,7 +152,7 @@
<dependency>
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm-agent</artifactId>
<version>4.9.1</version>
<version>4.9.2</version>
<scope>test</scope>
</dependency>
@@ -168,7 +168,7 @@
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.7.2</version>
<version>3.8.11.2</version>
<scope>test</scope>
</dependency>
@@ -219,7 +219,7 @@
<plugin>
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm-mavenenhancer</artifactId>
<version>4.8.1</version>
<version>4.9.2</version>
<executions>
<!-- Not going to enhance Model bean -->
<execution>
@@ -71,7 +71,7 @@ class DRawSqlParser {
preFrom = trimSelectKeyword(preFrom);
return new Sql(sql.hashCode(), preFrom, preWhere, whereExprAnd, preHaving, havingExprAnd, orderByPrefix, orderBySql, (distinctPos > -1));
return new Sql(sql, preFrom, preWhere, whereExprAnd, preHaving, havingExprAnd, orderByPrefix, orderBySql, (distinctPos > -1));
}
/**
@@ -0,0 +1,94 @@
package com.avaje.ebean;
/**
* Bean holding the details to update the document store.
*/
public final class DocStoreQueueEntry {
/**
* Action to either update or delete a document from the index.
*/
public enum Action {
/**
* Action is to update a document in the doc store.
*/
INDEX(1),
/**
* Action is to delete a document from the doc store..
*/
DELETE(2),
/**
* An update is required based on a change to a nested/embedded object at a given path.
*/
NESTED(3);
int value;
Action(int value) {
this.value = value;
}
/**
* Return the value associated with this action type.
*/
public int getValue() {
return value;
}
}
final Action type;
final String queueId;
final String path;
final Object beanId;
/**
* Construct for an INDEX or DELETE action.
*/
public DocStoreQueueEntry(Action type, String queueId, Object beanId) {
this(type, queueId, null, beanId);
}
/**
* Construct for an NESTED/embedded path invalidation action.
*/
public DocStoreQueueEntry(Action type, String queueId, String path, Object beanId) {
this.type = type;
this.queueId = queueId;
this.path = path;
this.beanId = beanId;
}
/**
* Return the event type.
*/
public Action getType() {
return type;
}
/**
* Return the associate queueId.
*/
public String getQueueId() {
return queueId;
}
/**
* Return the path if this is a nested update.
*/
public String getPath() {
return path;
}
/**
* Return the bean id (which matches the document id).
*/
public Object getBeanId() {
return beanId;
}
}
@@ -0,0 +1,270 @@
package com.avaje.ebean;
import com.avaje.ebeanservice.docstore.api.DocQueryRequest;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.List;
/**
* Document storage operations.
*/
public interface DocumentStore {
/**
* Update the associated document store using the result of the query.
* <p>
* This will execute the query against the database creating a document for each
* bean graph and sending this to the document store.
* </p>
* <p>
* Note that the select and fetch paths of the query is set for you to match the
* document structure needed based on <code>@DocStore</code> and <code>@DocStoreEmbedded</code>
* so what this query requires is the predicates only.
* </p>
* <p>
* This query will be executed using findEach so it is safe to use a query
* that will fetch a lot of beans. The default bulkBatchSize is used.
* </p>
*
* @param query The query that selects object to send to the document store.
*/
<T> void indexByQuery(Query<T> query);
/**
* Update the associated document store index using the result of the query additionally specifying a
* bulkBatchSize to use for sending the messages to ElasticSearch.
*
* @param query The query that selects object to send to the document store.
* @param bulkBatchSize The batch size to use when bulk sending to the document store.
*/
<T> void indexByQuery(Query<T> query, int bulkBatchSize);
/**
* Update the document store for all beans of this type.
* <p>
* This is the same as indexByQuery where the query has no predicates and so fetches all rows.
* </p>
*/
void indexAll(Class<?> beanType);
/**
* Return the bean by fetching it's content from the document store.
* If the document is not found null is returned.
* <p>
* Typically this is called indirectly by findUnique() on the query.
* </p>
* <pre>{@code
*
* Customer customer =
* server.find(Customer.class)
* .setUseDocStore(true)
* .setId(42)
* .findUnique();
*
* }</pre>
*/
@Nullable
<T> T find(DocQueryRequest<T> request);
/**
* Execute the find list query. This request is prepared to execute secondary queries.
* <p>
* Typically this is called indirectly by findList() on the query that has setUseDocStore(true).
* </p>
* <pre>{@code
*
* List<Customer> newCustomers =
* server.find(Customer.class)
* .setUseDocStore(true)
* .where().eq("status, Customer.Status.NEW)
* .findList();
*
* }</pre>
*/
<T> List<T> findList(DocQueryRequest<T> request);
/**
* Execute the query against the document store returning the paged list.
* <p>
* The query should have <code>firstRow</code> or <code>maxRows</code> set prior to calling this method.
* </p>
* <p>
* Typically this is called indirectly by findPagedList() on the query that has setUseDocStore(true).
* </p>
*
* <pre>{@code
*
* PagedList<Customer> newCustomers =
* server.find(Customer.class)
* .setUseDocStore(true)
* .where().eq("status, Customer.Status.NEW)
* .setMaxRows(50)
* .findPagedList();
*
* }</pre>
*
*/
<T> PagedList<T> findPagedList(DocQueryRequest<T> request);
/**
* Execute the query against the document store with the expectation of a large set of results
* that are processed in a scrolling resultSet fashion.
* <p>
* For example, with the ElasticSearch doc store this uses SCROLL.
* </p>
* <p>
* Typically this is called indirectly by findEach() on the query that has setUseDocStore(true).
* </p>
*
* <pre>{@code
*
* server.find(Order.class)
* .setUseDocStore(true)
* .where()... // perhaps add predicates
* .findEach(new QueryEachConsumer<Order>() {
* @Override
* public void accept(Order bean) {
* // process the bean
* }
* });
*
* }</pre>
*/
<T> void findEach(DocQueryRequest<T> query, QueryEachConsumer<T> consumer);
/**
* Execute the query against the document store with the expectation of a large set of results
* that are processed in a scrolling resultSet fashion.
* <p>
* Unlike findEach() this provides the opportunity to stop iterating through the large query.
* </p>
* <p>
* For example, with the ElasticSearch doc store this uses SCROLL.
* </p>
* <p>
* Typically this is called indirectly by findEachWhile() on the query that has setUseDocStore(true).
* </p>
*
*
* <pre>{@code
*
* server.find(Order.class)
* .setUseDocStore(true)
* .where()... // perhaps add predicates
* .findEachWhile(new QueryEachWhileConsumer<Order>() {
* @Override
* public void accept(Order bean) {
* // process the bean
*
* // return true to continue, false to stop
* // boolean shouldContinue = ...
* return shouldContinue;
* }
* });
*
* }</pre>
*/
<T> void findEachWhile(DocQueryRequest<T> query, QueryEachWhileConsumer<T> consumer);
/**
* Process the queue entries sending updates to the document store or queuing them for later processing.
*/
long process(List<DocStoreQueueEntry> queueEntries) throws IOException;
/**
* Drop the index from the document store (similar to DDL drop table).
*
* <pre>{@code
*
* DocumentStore documentStore = server.docStore();
*
* documentStore.dropIndex("product_copy");
*
* }</pre>
*
*/
void dropIndex(String indexName);
/**
* Create an index given a mapping file as a resource in the classPath (similar to DDL create table).
*
* <pre>{@code
*
* DocumentStore documentStore = server.docStore();
*
* // uses product_copy.mapping.json resource
* // ... to define mappings for the index
*
* documentStore.createIndex("product_copy", null);
*
* }</pre>
*
* @param indexName the name of the new index
* @param alias the alias of the index
*/
void createIndex(String indexName, String alias);
/**
* Copy the index to a new index.
* <p>
* This copy process does not use the database but instead will copy from the source index to a destination index.
* </p>
*
* <pre>{@code
*
* long copyCount = documentStore.copyIndex(Product.class, "product_copy");
*
* }</pre>
*
* @param beanType The bean type of the source index
* @param newIndex The name of the index to copy to
* @return the number of documents copied to the new index
*/
long copyIndex(Class<?> beanType, String newIndex);
/**
* Copy entries from an index to a new index but limiting to documents that have been
* modified since the sinceEpochMillis time.
* <p>
* To support this the document needs to have a <code>@WhenModified</code> property.
* </p>
*
* <pre>{@code
*
* long copyCount = documentStore.copyIndex(Product.class, "product_copy", sinceMillis);
*
* }</pre>
*
* @param beanType The bean type of the source index
* @param newIndex The name of the index to copy to
* @return the number of documents copied to the new index
*/
long copyIndex(Class<?> beanType, String newIndex, long sinceEpochMillis);
/**
* Copy from a source index to a new index taking only the documents
* matching the given query.
*
* <pre>{@code
*
* // predicates to select the source documents to copy
* Query<Product> query = server.find(Product.class)
* .where()
* .ge("whenModified", new Timestamp(since))
* .ge("name", "A")
* .lt("name", "D")
* .query();
*
* // copy from the source index to "product_copy" index
* long copyCount = documentStore.copyIndex(query, "product_copy", 1000);
*
* }</pre>
*
* @param query The query to select the source documents to copy
* @param newIndex The target index to copy the documents to
* @param bulkBatchSize The ElasticSearch bulk batch size, if 0 uses the default.
* @return The number of documents copied to the new index.
*/
long copyIndex(Query<?> query, String newIndex, int bulkBatchSize);
}
@@ -1886,11 +1886,16 @@ public interface EbeanServer {
*
* }</pre>
*
* @see com.avaje.ebean.text.PathProperties
* @see Query#apply(com.avaje.ebean.text.PathProperties)
* @see FetchPath
* @see Query#apply(FetchPath)
*/
JsonContext json();
/**
* Return the Document store.
*/
DocumentStore docStore();
/**
* Publish a single bean given its type and id returning the resulting live bean.
* <p>
@@ -1,10 +1,8 @@
package com.avaje.ebean;
import java.io.Serializable;
/**
* An expression that is part of a WHERE or HAVING clause.
*/
public interface Expression extends Serializable {
public interface Expression {
}
@@ -1,5 +1,11 @@
package com.avaje.ebean;
import com.avaje.ebean.search.Match;
import com.avaje.ebean.search.MultiMatch;
import com.avaje.ebean.search.TextCommonTerms;
import com.avaje.ebean.search.TextQueryString;
import com.avaje.ebean.search.TextSimple;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -285,6 +291,31 @@ public interface ExpressionFactory {
*/
Expression raw(String raw);
/**
* Create a Text Match expression (currently doc store/Elastic only).
*/
Expression textMatch(String propertyName, String search, Match options);
/**
* Create a Text Multi match expression (currently doc store/Elastic only).
*/
Expression textMultiMatch(String query, MultiMatch options);
/**
* Create a text simple query expression (currently doc store/Elastic only).
*/
Expression textSimple(String search, TextSimple options);
/**
* Create a text query string expression (currently doc store/Elastic only).
*/
Expression textQueryString(String search, TextQueryString options);
/**
* Create a text common terms expression (currently doc store/Elastic only).
*/
Expression textCommonTerms(String search, TextCommonTerms options);
/**
* And - join two expressions with a logical and.
*/
@@ -319,4 +350,13 @@ public interface ExpressionFactory {
* Return a list of expressions that will be joined by OR's.
*/
<T> Junction<T> disjunction(Query<T> query, ExpressionList<T> parent);
/**
* Return a Text query junction for MUST, SHOULD or MUST NOT.
* <p>
* This is doc store Elastic only.
* </p>
*/
<T> Junction<T> junction(Junction.Type type, Query<T> query, ExpressionList<T> parent);
}
@@ -1,10 +1,13 @@
package com.avaje.ebean;
import com.avaje.ebean.text.PathProperties;
import com.avaje.ebean.search.Match;
import com.avaje.ebean.search.MultiMatch;
import com.avaje.ebean.search.TextCommonTerms;
import com.avaje.ebean.search.TextQueryString;
import com.avaje.ebean.search.TextSimple;
import org.jetbrains.annotations.Nullable;
import javax.persistence.NonUniqueResultException;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
@@ -31,7 +34,7 @@ import java.util.Set;
*
* @see Query#where()
*/
public interface ExpressionList<T> extends Serializable {
public interface ExpressionList<T> {
/**
* Return the query that owns this expression list.
@@ -95,7 +98,7 @@ public interface ExpressionList<T> extends Serializable {
/**
* Apply the path properties to the query replacing the select and fetch clauses.
*/
Query<T> apply(PathProperties pathProperties);
Query<T> apply(FetchPath fetchPath);
/**
* Perform an 'As of' query using history tables to return the object graph
@@ -381,6 +384,32 @@ public interface ExpressionList<T> extends Serializable {
*/
Query<T> setUseQueryCache(boolean useCache);
/**
* Set to true if this query should execute against the doc store.
* <p>
* When setting this you may also consider disabling lazy loading.
* </p>
*/
Query<T> setUseDocStore(boolean useDocsStore);
/**
* Set true if you want to disable lazy loading.
* <p>
* That is, once the object graph is returned further lazy loading is disabled.
* </p>
*/
Query<T> setDisableLazyLoading(boolean disableLazyLoading);
/**
* Disable read auditing for this query.
* <p>
* This is intended to be used when the query is not a user initiated query and instead
* part of the internal processing in an application to load a cache or document store etc.
* In these cases we don't want the query to be part of read auditing.
* </p>
*/
Query<T> setDisableReadAuditing();
/**
* Add expressions to the having clause.
* <p>
@@ -803,6 +832,47 @@ public interface ExpressionList<T> extends Serializable {
*/
ExpressionList<T> raw(String raw);
/**
* Add a match expression.
*
* @param propertyName The property name for the match
* @param search The search value
*/
ExpressionList<T> match(String propertyName, String search);
/**
* Add a match expression with options.
*
* @param propertyName The property name for the match
* @param search The search value
*/
ExpressionList<T> match(String propertyName, String search, Match options);
/**
* Add a multi-match expression.
*/
ExpressionList<T> multiMatch(String search, String... properties);
/**
* Add a multi-match expression using options.
*/
ExpressionList<T> multiMatch(String search, MultiMatch options);
/**
* Add a simple query string expression.
*/
ExpressionList<T> textSimple(String search, TextSimple options);
/**
* Add a query string expression.
*/
ExpressionList<T> textQueryString(String search, TextQueryString options);
/**
* Add common terms expression.
*/
ExpressionList<T> textCommonTerms(String search, TextCommonTerms options);
/**
* And - join two expressions with a logical and.
*/
@@ -819,20 +889,168 @@ public interface ExpressionList<T> extends Serializable {
ExpressionList<T> not(Expression exp);
/**
* Return a list of expressions that will be joined by AND's.
* Start a list of expressions that will be joined by AND's
* returning the expression list the expressions are added to.
* <p>
* This is exactly the same as conjunction();
* </p>
* <p>
* Use endJunction() to end the AND junction.
* </p>
* <p>
* Note that a where() clause defaults to an AND junction so
* typically you only explicitly need to use the and() junction
* when it is nested inside an or() or not() junction.
* </p>
*
* <pre>{@code
*
* // Example: Nested and()
*
* Ebean.find(Customer.class)
* .where()
* .or()
* .and() // nested and
* .startsWith("name", "r")
* .eq("anniversary", onAfter)
* .endJunction() // end AND junction
* .and()
* .eq("status", Customer.Status.ACTIVE)
* .gt("id", 0)
* .endJunction() // end AND junction
* .orderBy().asc("name")
* .findList();
* }</pre>
*/
Junction<T> and();
/**
* Return a list of expressions that will be joined by OR's.
* This is exactly the same as disjunction();
*
* <p>
* Use endJunction() to end the OR junction.
* </p>
*
* <pre>{@code
*
* // Example: Use or() to join
* // two nested and() expressions
*
* Ebean.find(Customer.class)
* .where()
* .or()
* .and()
* .startsWith("name", "r")
* .eq("anniversary", onAfter)
* .endJunction() // end AND junction
* .and()
* .eq("status", Customer.Status.ACTIVE)
* .gt("id", 0)
* .endJunction() // end AND junction
* .orderBy().asc("name")
* .findList();
*
* }</pre>
*/
Junction<T> or();
/**
* Return a list of expressions that will be wrapped by NOT.
* <p>
* Use endJunction() to end expressions being added to the
* NOT expression list.
* </p>
*
* <pre>@{code
*
* .where()
* .not()
* .gt("id", 1)
* .eq("anniversary", onAfter)
* .endJunction() // end the not expressions
*
* }</pre>
*
* <pre>@{code
*
* // Example: nested not()
*
* Ebean.find(Customer.class)
* .where()
* .eq("status", Customer.Status.ACTIVE)
* .not()
* .gt("id", 1)
* .eq("anniversary", onAfter)
* .endJunction() // end the not expressions
* .orderBy()
* .asc("name")
* .findList();
*
* }</pre>
*/
Junction<T> not();
/**
* Start (and return) a list of expressions that will be joined by AND's.
* <p>
* This is the same as and().
* </p>
*/
Junction<T> conjunction();
/**
* Return a list of expressions that will be joined by OR's.
* Start (and return) a list of expressions that will be joined by OR's.
* <p>
* This is the same as or().
* </p>
*/
Junction<T> disjunction();
/**
* End a Conjunction or Disjunction returning the parent expression list.
* Start a list of expressions that will be joined by MUST.
* <p>
* Alternatively you can always use where() to return the top level expression
* list.
* This automatically makes the query a useDocStore(true) query that
* will execute against the document store (ElasticSearch etc).
* </p>
* <p>
* This is logically similar to and().
* </p>
*/
Junction<T> must();
/**
* Start a list of expressions that will be joined by SHOULD.
* <p>
* This automatically makes the query a useDocStore(true) query that
* will execute against the document store (ElasticSearch etc).
* </p>
* <p>
* This is logically similar to or().
* </p>
*/
Junction<T> should();
/**
* Start a list of expressions that will be joined by MUST NOT.
* <p>
* This automatically makes the query a useDocStore(true) query that
* will execute against the document store (ElasticSearch etc).
* </p>
* <p>
* This is logically similar to not().
* </p>
*/
Junction<T> mustNot();
/**
* End a junction returning the parent expression list.
* <p>
* Ends a and(), or(), not(), must(), mustNot() or should() junction
* such that you get the parent expression.
* </p>
* <p>
* Alternatively you can always use where() to return the top level expression list.
* </p>
*/
ExpressionList<T> endJunction();
@@ -249,4 +249,22 @@ public class FetchConfig implements Serializable {
return queryAll;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FetchConfig that = (FetchConfig) o;
if (lazyBatchSize != that.lazyBatchSize) return false;
if (queryBatchSize != that.queryBatchSize) return false;
return queryAll == that.queryAll;
}
@Override
public int hashCode() {
int result = lazyBatchSize;
result = 92821 * result + queryBatchSize;
result = 92821 * result + (queryAll ? 1 : 0);
return result;
}
}
@@ -0,0 +1,26 @@
package com.avaje.ebean;
import java.util.Set;
/**
* Provides paths and properties for an object graph that can be used to control what parts of the object graph
* is fetching (select and fetch clauses) and also can be used to control JSON marshalling (what parts of the object
* graph are included in the JSON).
*/
public interface FetchPath {
/**
* Return true if the path is included in this FetchPath.
*/
boolean hasPath(String path);
/**
* Return the properties at the given path.
*/
Set<String> getProperties(String path);
/**
* Apply the fetch path to the query.
*/
<T> void apply(Query<T> query);
}
+104 -45
View File
@@ -9,70 +9,129 @@ package com.avaje.ebean;
* <p>
* Note: where() always takes you to the top level WHERE expression list.
* </p>
*
* <pre class="code">
*
* <pre>{@code
* Query q =
* Ebean.find(Person.class)
* .where().disjunction()
* .like(&quot;name&quot;, &quot;Rob%&quot;)
* .eq(&quot;status&quot;, Status.NEW)
*
* // where() returns us to the top level expression list
* .where().gt(&quot;id&quot;, 10);
*
* .where()
* .or()
* .like("name", "Rob%")
* .eq("status", Status.NEW)
*
* // where() returns us to the top level expression list
* .where().gt("id", 10);
*
* // read as...
* // where ( ((name like Rob%) or (status = NEW)) AND (id &gt; 10) )
* </pre>
*
* }</pre>
*
* <p>
* Note: endJunction() takes you to the parent expression list
* </p>
*
* <pre class="code">
*
* <pre>{@code
* Query q =
* Ebean.find(Person.class)
* .where().disjunction()
* .like(&quot;name&quot;, &quot;Rob%&quot;)
* .eq(&quot;status&quot;, Status.NEW)
* .endJunction()
*
* // endJunction().. takes us to the 'parent' expression list
* // which in this case is the top level (same as where())
*
* .gt(&quot;id&quot;, 10);
*
* .where()
* .or()
* .like("name", "Rob%")
* .eq("status", Status.NEW)
* .endJunction()
*
* // endJunction().. takes us to the 'parent' expression list
* // which in this case is the top level (same as where())
*
* .gt("id", 10);
*
* // read as...
* // where ( ((name like Rob%) or (status = NEW)) AND (id &gt; 10) )
* </pre>
*
* // where ( ((name like Rob%) or (status = NEW)) AND (id > 10) )
* }</pre>
*
* <p>
* Example of a nested disjunction.
* </p>
*
* <pre class="code">
* Query&lt;Customer&gt; q =
*
* <pre>{@code
* Query<Customer> q =
* Ebean.find(Customer.class)
* .where()
* .disjunction()
* .conjunction()
* .startsWith(&quot;name&quot;, &quot;r&quot;)
* .eq(&quot;anniversary&quot;, onAfter)
* .endJunction()
* .conjunction()
* .eq(&quot;status&quot;, Customer.Status.ACTIVE)
* .gt(&quot;id&quot;, 0)
* .endJunction()
* .order().asc(&quot;name&quot;);
*
* .or()
* .and()
* .startsWith("name", "r")
* .eq("anniversary", onAfter)
* .endJunction()
* .and()
* .eq("status", Customer.Status.ACTIVE)
* .gt("id", 0)
* .endJunction()
* .order().asc("name");
*
* q.findList();
* String s = q.getGeneratedSql();
*
*
* // this produces an expression like:
*
* ( name like ? and c.anniversary = ? ) or (c.status = ? and c.id &gt; ? )
*
* </pre>
* ( name like ? and c.anniversary = ? ) or (c.status = ? and c.id > ? )
*
* }</pre>
*/
public interface Junction<T> extends Expression, ExpressionList<T> {
/**
* The type of Junction used in full text expressions.
*/
enum Type {
/**
* AND group.
*/
AND(" and ", ""),
/**
* OR group.
*/
OR(" or ", ""),
/**
* NOT group.
*/
NOT(" and ", "not "),
/**
* Text search AND group.
*/
MUST("must", ""),
/**
* Text search NOT group.
*/
MUST_NOT("must_not", ""),
/**
* Text search OR group.
*/
SHOULD("should", "");
String prefix;
String literal;
Type(String literal, String prefix) {
this.literal = literal;
this.prefix = prefix;
}
/**
* Return the literal value for this type.
*/
public String literal() {
return literal;
}
/**
* Return the prefix value for this type.
*/
public String prefix() {
return prefix;
}
}
}
+3 -4
View File
@@ -1,7 +1,6 @@
package com.avaje.ebean;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.text.PathProperties;
import com.avaje.ebean.util.ClassUtil;
import org.jetbrains.annotations.Nullable;
@@ -588,10 +587,10 @@ public abstract class Model {
/**
* Creates a query applying the path properties to set the select and fetch clauses.
* <p>
* Equivalent to {@link Query#apply(com.avaje.ebean.text.PathProperties)}
* Equivalent to {@link Query#apply(FetchPath)}
*/
public Query<T> apply(PathProperties pathProperties) {
return db().find(type).apply(pathProperties);
public Query<T> apply(FetchPath fetchPath) {
return db().find(type).apply(fetchPath);
}
/**
@@ -0,0 +1,18 @@
package com.avaje.ebean;
import javax.persistence.PersistenceException;
/**
* Captures and wraps IOException's occurring during ElasticSearch processing etc.
*/
public class PersistenceIOException extends PersistenceException {
public PersistenceIOException(String msg, Exception cause) {
super(msg, cause);
}
public PersistenceIOException(Exception cause) {
super(cause);
}
}
+33 -5
View File
@@ -1,10 +1,8 @@
package com.avaje.ebean;
import com.avaje.ebean.text.PathProperties;
import org.jetbrains.annotations.Nullable;
import javax.persistence.NonUniqueResultException;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.List;
import java.util.Map;
@@ -275,7 +273,7 @@ import java.util.Set;
* @param <T>
* the type of Entity bean this query will fetch.
*/
public interface Query<T> extends Serializable {
public interface Query<T> {
/**
* Return the RawSql that was set to use for this query.
@@ -522,10 +520,10 @@ public interface Query<T> extends Serializable {
/**
* Apply the path properties replacing the select and fetch clauses.
* <p>
* This is typically used when the PathProperties is applied to both the query and the JSON output.
* This is typically used when the FetchPath is applied to both the query and the JSON output.
* </p>
*/
Query<T> apply(PathProperties pathProperties);
Query<T> apply(FetchPath fetchPath);
/**
* Execute the query returning the list of Id's.
@@ -1043,6 +1041,27 @@ public interface Query<T> extends Serializable {
*/
ExpressionList<T> where();
/**
* Add Full text search expressions for Document store queries.
* <p>
* This is currently ElasticSearch only and provides the full text
* expressions such as Match and Multi-Match.
* </p>
* <p>
* This automatically makes this query a "Doc Store" query and will execute
* against the document store (ElasticSearch).
* </p>
* <p>
* Expressions added here are added to the "query" section of an ElasticSearch
* query rather than the "filter" section.
* </p>
* <p>
* Expressions added to the where() are added to the "filter" section of an
* ElasticSearch query.
* </p>
*/
ExpressionList<T> text();
/**
* This applies a filter on the 'many' property list rather than the root
* level objects.
@@ -1286,6 +1305,14 @@ public interface Query<T> extends Serializable {
*/
Query<T> setUseQueryCache(boolean useQueryCache);
/**
* Set to true if this query should execute against the doc store.
* <p>
* When setting this you may also consider disabling lazy loading.
* </p>
*/
Query<T> setUseDocStore(boolean useDocStore);
/**
* When set to true when you want the returned beans to be read only.
*/
@@ -1365,4 +1392,5 @@ public interface Query<T> extends Serializable {
* </p>
*/
Set<String> validate();
}
+83 -47
View File
@@ -206,6 +206,14 @@ public final class RawSql implements Serializable {
return sql;
}
/**
* Return the key;
*/
public Key getKey() {
boolean parsed = sql != null && sql.parsed;
String unParsedSql = (sql == null) ? "" : sql.unparsedSql;
return new Key(parsed, unParsedSql, columnMapping);
}
/**
* Return the resultSet if this is a ResultSet based RawSql.
@@ -221,16 +229,6 @@ public final class RawSql implements Serializable {
return columnMapping;
}
/**
* Return the hash for this query.
*/
public int queryHash() {
if (resultSet != null) {
return 31 * columnMapping.queryHash();
}
return 31 * sql.queryHash() + columnMapping.queryHash();
}
/**
* Represents the sql part of the query. For parsed RawSql the sql is broken
* up so that Ebean can insert extra WHERE and HAVING expressions into the
@@ -260,13 +258,10 @@ public final class RawSql implements Serializable {
private final boolean distinct;
private final int queryHashCode;
/**
* Construct for unparsed SQL.
*/
protected Sql(String unparsedSql) {
this.queryHashCode = unparsedSql.hashCode();
this.parsed = false;
this.unparsedSql = unparsedSql;
this.preFrom = null;
@@ -282,12 +277,11 @@ public final class RawSql implements Serializable {
/**
* Construct for parsed SQL.
*/
protected Sql(int queryHashCode, String preFrom, String preWhere, boolean andWhereExpr,
protected Sql(String unparsedSql, String preFrom, String preWhere, boolean andWhereExpr,
String preHaving, boolean andHavingExpr, String orderByPrefix, String orderBy, boolean distinct) {
this.queryHashCode = queryHashCode;
this.unparsedSql = unparsedSql;
this.parsed = true;
this.unparsedSql = null;
this.preFrom = preFrom;
this.preHaving = preHaving;
this.preWhere = preWhere;
@@ -298,13 +292,6 @@ public final class RawSql implements Serializable {
this.distinct = distinct;
}
/**
* Return a hash for this query.
*/
public int queryHash() {
return queryHashCode;
}
public String toString() {
if (!parsed) {
return "unparsed[" + unparsedSql + "]";
@@ -406,13 +393,10 @@ public final class RawSql implements Serializable {
private final boolean immutable;
private final int queryHashCode;
/**
* Construct from parsed sql where the columns have been identified.
*/
protected ColumnMapping(List<Column> columns) {
this.queryHashCode = 0;
this.immutable = false;
this.parsed = true;
this.propertyMap = null;
@@ -428,7 +412,6 @@ public final class RawSql implements Serializable {
* Construct for unparsed sql.
*/
protected ColumnMapping() {
this.queryHashCode = 0;
this.immutable = false;
this.parsed = false;
this.propertyMap = null;
@@ -443,17 +426,13 @@ public final class RawSql implements Serializable {
this.immutable = false;
this.parsed = false;
this.propertyMap = null;
//this.propertyColumnMap = null;
this.dbColumnMap = new LinkedHashMap<String, Column>();
int hc = 31;
int pos = 0;
for (String prop : propertyNames) {
hc = 31 * hc + prop.hashCode();
dbColumnMap.put(prop, new Column(pos++, prop, null, prop));
}
propertyColumnMap = dbColumnMap;
this.queryHashCode = hc;
}
/**
@@ -464,20 +443,28 @@ public final class RawSql implements Serializable {
this.parsed = parsed;
this.dbColumnMap = dbColumnMap;
int hc = ColumnMapping.class.getName().hashCode();
HashMap<String, Column> pcMap = new HashMap<String, Column>();
HashMap<String, String> pMap = new HashMap<String, String>();
for (Column c : dbColumnMap.values()) {
pMap.put(c.getPropertyName(), c.getDbColumn());
pcMap.put(c.getPropertyName(), c);
hc = 31 * hc + ((c.getPropertyName() == null) ? 0 : c.getPropertyName().hashCode());
hc = 31 * hc + ((c.getDbColumn() == null) ? 0 : c.getDbColumn().hashCode());
}
this.propertyMap = Collections.unmodifiableMap(pMap);
this.propertyColumnMap = Collections.unmodifiableMap(pcMap);
this.queryHashCode = hc;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ColumnMapping that = (ColumnMapping) o;
return dbColumnMap.equals(that.dbColumnMap);
}
@Override
public int hashCode() {
return dbColumnMap.hashCode();
}
/**
@@ -521,16 +508,6 @@ public final class RawSql implements Serializable {
}
}
/**
* Return the query hash for this column mapping.
*/
public int queryHash() {
if (queryHashCode == 0) {
throw new RuntimeException("Bug: queryHashCode == 0");
}
return queryHashCode;
}
/**
* Returns true if the Columns where supplied by parsing the sql select
* clause.
@@ -585,6 +562,9 @@ public final class RawSql implements Serializable {
* <p>
* For example modify all mappings with table alias "c" to have the path prefix "customer".
* </p>
* <p>
* For the "Root type" you don't need to specify a tableAliasMapping.
* </p>
*/
public void tableAliasMapping(String tableAlias, String path) {
@@ -645,6 +625,27 @@ public final class RawSql implements Serializable {
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Column that = (Column) o;
if (indexPos != that.indexPos) return false;
if (!dbColumn.equals(that.dbColumn)) return false;
if (dbAlias != null ? !dbAlias.equals(that.dbAlias) : that.dbAlias != null) return false;
return propertyName != null ? propertyName.equals(that.propertyName) : that.propertyName == null;
}
@Override
public int hashCode() {
int result = indexPos;
result = 31 * result + dbColumn.hashCode();
result = 31 * result + (dbAlias != null ? dbAlias.hashCode() : 0);
result = 31 * result + (propertyName != null ? propertyName.hashCode() : 0);
return result;
}
public String toString() {
return dbColumn + "->" + propertyName;
}
@@ -696,4 +697,39 @@ public final class RawSql implements Serializable {
}
}
}
/**
* A key for the RawSql object using for the query plan.
*/
public static final class Key {
private final boolean parsed;
private final ColumnMapping columnMapping;
private final String unParsedSql;
Key(boolean parsed, String unParsedSql, ColumnMapping columnMapping) {
this.parsed = parsed;
this.unParsedSql = unParsedSql;
this.columnMapping = columnMapping;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key that = (Key) o;
return parsed == that.parsed
&& columnMapping.equals(that.columnMapping)
&& unParsedSql.equals(that.unParsedSql);
}
@Override
public int hashCode() {
int result = (parsed ? 1 : 0);
result = 31 * result + columnMapping.hashCode();
result = 31 * result + unParsedSql.hashCode();
return result;
}
}
}
@@ -106,6 +106,9 @@ public class RawSqlBuilder {
* <p>
* For example modify all mappings with table alias "c" to have the path prefix "customer".
* </p>
* <p>
* For the "Root type" you don't need to specify a tableAliasMapping.
* </p>
*/
public RawSqlBuilder tableAliasMapping(String tableAlias, String path) {
columnMapping.tableAliasMapping(tableAlias, path);
@@ -1,5 +1,7 @@
package com.avaje.ebean;
import com.avaje.ebean.annotation.DocStoreMode;
import com.avaje.ebean.config.DocStoreConfig;
import com.avaje.ebean.config.PersistBatch;
import javax.persistence.PersistenceException;
@@ -81,6 +83,29 @@ public interface Transaction extends Closeable {
*/
boolean isActive();
/**
* Set the behavior for document store updates on this transaction.
* <p>
* For example, set the mode to DocStoreEvent.IGNORE for this transaction and
* then any changes via this transaction are not sent to the doc store. This
* would be used when doing large bulk inserts into the database and we want
* to control how that is sent to the document store.
* </p>
*/
void setDocStoreMode(DocStoreMode mode);
/**
* Set the batch size to use for sending messages to the document store.
* <p>
* You might set this if you know the changes in this transaction result in especially large or
* especially small payloads and want to adjust the batch size to match.
* </p>
* <p>
* Setting this overrides the default of {@link DocStoreConfig#getBulkBatchSize()}
* </p>
*/
void setDocStoreBatchSize(int batchSize);
/**
* Explicitly turn off or on the cascading nature of save() and delete(). This
* gives the developer exact control over what beans are saved and deleted
@@ -0,0 +1,32 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Used to indicate that a particular string property should be treated as a 'code' and not analysed for text searching.
* <p>
* By default all Id properties and all Enum properties are treated as 'code' and not analysed.
* </p>
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DocCode {
/**
* Set to true to have the property additionally stored separately from _source.
*/
boolean store() default false;
/**
* Set a boost value specific to this property.
*/
float boost() default 1;
/**
* Set a value to use instead of null.
*/
String nullValue() default "";
}
@@ -0,0 +1,24 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specify the entity type maps to a document store (like ElasticSearch).
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DocMapping {
/**
* The property name the mapping applies to.
*/
String name();
/**
* Mapping options.
*/
DocProperty options();
}
@@ -0,0 +1,110 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specify the entity type maps to a document store (like ElasticSearch).
*/
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DocProperty {
/**
* Set this to true to indicate that this property should be un-analysed.
*/
boolean code() default false;
/**
* Set this to true to get an additional un-analysed 'raw' field to use for sorting etc.
*/
boolean sortable() default false;
/**
* Set to true to have the property additionally stored separately from _source.
*/
boolean store() default false;
/**
* Set a boost value specific to this property.
*/
float boost() default 1;
/**
* Set a value to use instead of null.
*/
String nullValue() default "";
/**
* Set this to false to exclude this from the _all property.
*/
boolean includeInAll() default true;
/**
* The analyzer to use.
*/
String analyzer() default "";
/**
* The analyzer to use for searches.
*/
String searchAnalyzer() default "";
/**
* The index options for this property.
*/
Option options() default Option.DEFAULT;
/**
* Set this to false such that doc values are not stored separately for this property.
*/
boolean docValues() default true;
/**
* Set a copyTo field.
*/
String copyTo() default "";
/**
* Set to false to disable the field from indexing, it will only be get/set in _source.
*/
boolean enabled() default true;
/**
* Set to false such that norms are not stored.
*/
boolean norms() default true;
/**
* Index options for a property.
*/
enum Option {
/**
* Only index the doc number.
*/
DOCS,
/**
* Doc number and term frequencies are indexed.
*/
FREQS,
/**
* Doc number, term frequencies and term positions are indexed.
*/
POSITIONS,
/**
* Doc number, term frequencies, term positions and start/end offsets are indexed.
*/
OFFSETS,
/**
* Use the default which means analysed string properties use POSITIONS as the default and all other types use DOCS.
*/
DEFAULT
}
}
@@ -0,0 +1,34 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Used to indicate that a particular string property should support sorting. What this typically means is that
* for ElasticSearch an additional 'raw' field is added that stores the un-analysed value. This un-analysed value
* can be used for sorting etc and the original field used for text searching.
* <p>
* For example, customer name and product name are good candidates for marking with @DocSortable.
* </p>
*/
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DocSortable {
/**
* Set to true to have the property additionally stored separately from _source.
*/
boolean store() default false;
/**
* Set a boost value specific to this property.
*/
float boost() default 1;
/**
* Set a value to use instead of null.
*/
String nullValue() default "";
}
@@ -0,0 +1,88 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specify the entity type maps to a document store (like ElasticSearch).
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DocStore {
/**
* A unique Id used when queuing reindex events.
*/
String queueId() default "";
/**
* The ElasticSearch index name. If left unspecified the short name of the bean type is used.
*/
String indexName() default "";
/**
* The ElasticSearch index type. If left unspecified the short name of the bean type is used.
*/
String indexType() default "";
/**
* The number of shards this index should use.
*/
int shards() default 0;
/**
* The number of replicas this index should use.
*/
int replicas() default 0;
/**
* Additional mapping that can be defined on the properties.
*/
DocMapping[] mapping() default {};
/**
* Specify the behavior when bean Insert, Update, Delete events occur.
*/
DocStoreMode persist() default DocStoreMode.DEFAULT;
/**
* Specify the behavior when bean Insert occurs.
*/
DocStoreMode insert() default DocStoreMode.DEFAULT;
/**
* Specify the behavior when bean Update occurs.
*/
DocStoreMode update() default DocStoreMode.DEFAULT;
/**
* Specify the behavior when bean Delete occurs.
*/
DocStoreMode delete() default DocStoreMode.DEFAULT;
/**
* Specify to include only some properties in the doc store document.
* <p>
* If this is left as default then all scalar properties are included,
* all @ManyToOne properties are included with just the nested id property
* and no @OneToMany properties are included.
* </p>
* <p>
* Note that typically DocStoreEmbedded is used on @ManyToOne and @OneToMany
* properties to indicate what part of the nested document should be included.
* </p>
*
* <h3>Example:</h3>
* <pre>{@code
*
* // only include the customer id and name
* @DocStore(doc = "id,name")
* @Entity @Table(name = "o_order")
* public class Customer {
*
* }</pre>
*/
String doc() default "";
}
@@ -0,0 +1,37 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specify the property is included in the parent document store index.
*
* <pre>{@code
*
*
* @DocStore
* @Entity @Table(name = "o_order")
* public class Order {
*
* ...
* // include some customer details including
* // nested billingAddress
* @DocStoreEmbedded(doc = "id,status,name,billingAddress(*,country(*)")
* @ManyToOne
* Customer customer;
*
*
* }</pre>
*/
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DocStoreEmbedded {
/**
* The properties on the embedded bean to include in the index.
*/
String doc() default "";
}
@@ -0,0 +1,38 @@
package com.avaje.ebean.annotation;
/**
* Defines the behavior options when a Insert, Update or Delete event occurs
* on a bean with an associated ElasticSearch index.
* <p>
* For some indexes or some transactions it can be beneficial to queue the event
* for later processing rather than look to update ElasticSearch at that time.
* </p>
*/
public enum DocStoreMode {
/**
* Add the event to the queue for processing later (delaying the update to the document store).
*/
QUEUE,
/**
* Update the document store when transaction succeeds.
*/
UPDATE,
/**
* Ignore the event and not update the document store.
* <p>
* This can be used on a index or for a transaction where you want to have more
* manual programmatic control over the updating of the document store. Say you want to
* IGNORE on a particular transaction and instead manually queue a bulk update.
* </p>
*/
IGNORE,
/**
* The actual mode of QUEUE, UPDATE or IGNORE is set from the default configuration.
*/
DEFAULT
}
@@ -31,6 +31,11 @@ public interface BeanCollection<E> extends Serializable {
ALL
}
/**
* Load bean from another collection.
*/
void loadFrom(BeanCollection<?> other);
/**
* Add a bean to the list/set with modifyListen notification.
*/
@@ -125,6 +130,11 @@ public interface BeanCollection<E> extends Serializable {
*/
void internalAdd(Object bean);
/**
* Add the bean with a check to see if it is already contained.
*/
void internalAddWithCheck(Object bean);
/**
* Return the number of elements in the List Set or Map.
*/
@@ -219,6 +219,18 @@ public final class EntityBeanIntercept implements Serializable {
this.fullyLoadedBean = fullyLoadedBean;
}
/**
* Check each property to see if the bean is partially loaded.
*/
public boolean isPartial() {
for (int i = 0; i < loadedProps.length; i++) {
if (!loadedProps[i]) {
return true;
}
}
return false;
}
/**
* Return true if this bean has been directly modified (it has oldValues) or
* if any embedded beans are either new or dirty (and hence need saving).
@@ -343,28 +355,10 @@ public final class EntityBeanIntercept implements Serializable {
}
/**
* Check if the lazy load succeeded. If not then mark this bean as having
* failed lazy loading due to the underlying row being deleted.
* <p>
* We mark the bean this way rather than immediately fail as we might be batch
* lazy loading and this bean might not be used by the client code at all.
* Instead we will fail as soon as the client code tries to use this bean.
* </p>
* @param lazyLoadPropertyIndex the property that is expected to be loaded
* Set lazy load failure flag.
*/
public boolean isLazyLoadFailure(int lazyLoadPropertyIndex) {
if (lazyLoadProperty != -1 || !isLoadedProperty(lazyLoadPropertyIndex)) {
lazyLoadFailure = true;
return true;
}
lazyLoadFailure = false;
return false;
}
/**
* Set the Id of the owner bean.
*/
public void setOwnerId(Object ownerId) {
public void setLazyLoadFailure(Object ownerId) {
this.lazyLoadFailure = true;
this.ownerId = ownerId;
}
@@ -487,7 +481,7 @@ public final class EntityBeanIntercept implements Serializable {
public void setLoadedProperty(int propertyIndex) {
loadedProps[propertyIndex] = true;
}
/**
* Return true if the property is loaded.
*/
@@ -578,6 +572,23 @@ public final class EntityBeanIntercept implements Serializable {
return props;
}
/**
* Return the array of flags indicating the dirty properties.
*/
public boolean[] getDirtyProperties() {
int len = getPropertyLength();
boolean[] dirties = new boolean[len];
for (int i = 0; i < len; i++) {
if (changedProps != null && changedProps[i]) {
dirties[i] = true;
} else if (embeddedDirty != null && embeddedDirty[i]) {
// an embedded property has been changed - recurse
dirties[i] = true;
}
}
return dirties;
}
/**
* Return the set of dirty properties.
*/
@@ -10,31 +10,31 @@ package com.avaje.ebean.bean;
public interface PersistenceContext {
/**
* Put the entity bean into the PersistanceContext.
* Put the entity bean into the PersistenceContext.
*/
void put(Object id, Object bean);
void put(Class<?> rootType, Object id, Object bean);
/**
* Put the entity bean into the PersistanceContext if one is not already
* Put the entity bean into the PersistenceContext if one is not already
* present (for this id).
* <p>
* Returns an existing entity bean (if one is already there) and otherwise
* returns null.
* </p>
*/
Object putIfAbsent(Object id, Object bean);
Object putIfAbsent(Class<?> rootType, Object id, Object bean);
/**
* Return an object given its type and unique id.
*/
Object get(Class<?> beanType, Object uid);
Object get(Class<?> rootType, Object uid);
/**
* Get the bean from the persistence context also checked to see if it had
* been previously deleted (if so then you also can't hit the L2 cache to
* fetch the bean for this particular persistence context).
*/
WithOption getWithOption(Class<?> beanType, Object uid);
WithOption getWithOption(Class<?> rootType, Object uid);
/**
* Clear all the references.
@@ -44,22 +44,22 @@ public interface PersistenceContext {
/**
* Clear all the references for a given type of entity bean.
*/
void clear(Class<?> beanType);
void clear(Class<?> rootType);
/**
* Clear the reference to a specific entity bean.
*/
void clear(Class<?> beanType, Object uid);
void clear(Class<?> rootType, Object uid);
/**
* Clear the reference as a result of an entity being deleted.
*/
void deleted(Class<?> beanType, Object id);
void deleted(Class<?> rootType, Object id);
/**
* Return the number of beans of the given type in the persistence context.
*/
int size(Class<?> beanType);
int size(Class<?> rootType);
/**
* Wrapper on a bean to also indicate if a bean has been deleted.
@@ -0,0 +1,21 @@
package com.avaje.ebean.bean;
import javax.persistence.Entity;
/**
* Utility to find the root bean type.
*/
public class PersistenceContextUtil {
/**
* Find and return the root bean type for the given class.
*/
public static Class<?> root(Class<?> beanType) {
Class<?> parent = beanType.getSuperclass();
while (parent != null && parent.isAnnotationPresent(Entity.class)) {
beanType = parent;
parent = parent.getSuperclass();
}
return beanType;
}
}
@@ -1,5 +1,10 @@
package com.avaje.ebean.common;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.ebean.bean.BeanCollectionAdd;
import com.avaje.ebean.bean.BeanCollectionLoader;
import com.avaje.ebean.bean.EntityBean;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
@@ -8,17 +13,13 @@ import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import com.avaje.ebean.bean.BeanCollectionAdd;
import com.avaje.ebean.bean.BeanCollectionLoader;
import com.avaje.ebean.bean.EntityBean;
/**
* List capable of lazy loading.
*/
public final class BeanList<E> extends AbstractBeanCollection<E> implements List<E>, BeanCollectionAdd {
private static final long serialVersionUID = 1L;
/**
* The underlying List implementation.
*/
@@ -64,6 +65,15 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
list.add((E) bean);
}
@Override
@SuppressWarnings("unchecked")
public void loadFrom(BeanCollection<?> other) {
if (list == null) {
list = new ArrayList<E>();
}
list.addAll((Collection<? extends E>) other.getActualDetails());
}
@SuppressWarnings("unchecked")
public void internalAdd(Object bean) {
if (list == null) {
@@ -74,6 +84,13 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
}
}
@Override
public void internalAddWithCheck(Object bean) {
if (list == null || !list.contains(bean)) {
internalAdd(bean);
}
}
public boolean checkEmptyLazyLoad() {
if (list == null) {
list = new ArrayList<E>();
@@ -99,11 +116,11 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
private void initAsUntouched() {
init(false);
}
private void init() {
init(true);
}
private void init(boolean setTouched) {
synchronized (this) {
if (list == null) {
@@ -134,7 +151,7 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
public Collection<E> getActualDetails() {
return list;
}
@Override
public Collection<?> getActualEntries() {
return list;
@@ -1,5 +1,6 @@
package com.avaje.ebean.common;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.ebean.bean.BeanCollectionLoader;
import com.avaje.ebean.bean.EntityBean;
@@ -51,6 +52,14 @@ public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements Ma
return !touched && (map == null || map.isEmpty());
}
@Override
@SuppressWarnings("unchecked")
public void loadFrom(BeanCollection<?> other) {
BeanMap<K,E> otherMap = (BeanMap<K,E>)other;
internalPutNull();
map.putAll(otherMap.getActualMap());
}
public void internalPutNull() {
if (map == null) {
map = new LinkedHashMap<K, E>();
@@ -66,7 +75,18 @@ public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements Ma
map.put((K) key, (E) bean);
}
}
public void internalPutWithCheck(Object key, Object bean) {
if (map == null || !map.containsKey(key)) {
internalPut(key, bean);
}
}
@Override
public void internalAddWithCheck(Object bean) {
throw new RuntimeException("Not allowed for map");
}
public void internalAdd(Object bean) {
throw new RuntimeException("Not allowed for map");
}
@@ -1,15 +1,16 @@
package com.avaje.ebean.common;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.ebean.bean.BeanCollectionAdd;
import com.avaje.ebean.bean.BeanCollectionLoader;
import com.avaje.ebean.bean.EntityBean;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import com.avaje.ebean.bean.BeanCollectionAdd;
import com.avaje.ebean.bean.BeanCollectionLoader;
import com.avaje.ebean.bean.EntityBean;
/**
* Set capable of lazy loading.
*/
@@ -57,6 +58,22 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
set.add((E) bean);
}
@Override
@SuppressWarnings("unchecked")
public void loadFrom(BeanCollection<?> other) {
if (set == null) {
set = new LinkedHashSet<E>();
}
set.addAll((Collection<? extends E>) other.getActualDetails());
}
@Override
public void internalAddWithCheck(Object bean) {
if (set == null || !set.contains(bean)) {
internalAdd(bean);
}
}
@SuppressWarnings("unchecked")
public void internalAdd(Object bean) {
if (set == null) {
@@ -107,11 +124,11 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
private void initAsUntouched() {
init(false);
}
private void init() {
init(true);
}
private void init(boolean setTouched) {
synchronized (this) {
if (set == null) {
@@ -90,6 +90,13 @@ public class ClassLoadConfig {
return context.forName(name);
}
/**
* Return the classLoader to use for service loading etc.
*/
public ClassLoader getClassLoader() {
return context.getClassLoader();
}
/**
* Wraps the preferred, caller and context class loaders.
*/
@@ -138,6 +145,9 @@ public class ClassLoadConfig {
return Class.forName(name, true, classLoader);
}
ClassLoader getClassLoader() {
return preferredLoader != null ? preferredLoader : contextLoader;
}
}
}
@@ -23,11 +23,6 @@ public class DbMigrationConfig {
*/
protected boolean generate;
/**
* Set to true to suppress the output of the rollback script.
*/
protected boolean suppressRollback;
/**
* The migration version name (typically FlywayDb compatible).
* <p>
@@ -61,33 +56,17 @@ public class DbMigrationConfig {
*/
protected String modelPath = "model";
/**
* Subdirectory the drop ddl scripts go into.
*/
protected String dropPath = "drop";
/**
* Subdirectory the rollback ddl scripts go into.
*/
protected String rollbackPath = "rollback";
/**
* Apply script suffix.
*/
protected String applySuffix = ".sql";
/**
* Default drop script suffix to ddl so that it isn't picked up by FlywayDb.
*/
protected String dropSuffix = ".drop.ddl";
/**
* Default rollback script suffix to ddl so that it isn't picked up by FlywayDb.
*/
protected String rollbackSuffix = ".rollback.ddl";
protected String modelSuffix = ".model.xml";
protected boolean includeGeneratedFileComment;
/**
* The version of a pending drop that should be generated as the next migration.
*/
protected String generatePendingDrop;
/**
* Return the DB platform to generate migration DDL for.
*
@@ -138,34 +117,6 @@ public class DbMigrationConfig {
this.modelPath = modelPath;
}
/**
* Return the relative path for the drop ddl scripts (defaults to drop).
*/
public String getDropPath() {
return dropPath;
}
/**
* Set the relative path for the drop ddl scripts (defaults to drop).
*/
public void setDropPath(String dropPath) {
this.dropPath = dropPath;
}
/**
* Return the relative path for the rollback ddl scripts (defaults to rollback).
*/
public String getRollbackPath() {
return rollbackPath;
}
/**
* Set the relative path for the rollback ddl scripts (defaults to rollback).
*/
public void setRollbackPath(String rollbackPath) {
this.rollbackPath = rollbackPath;
}
/**
* Return the model suffix (defaults to model.xml)
*/
@@ -180,20 +131,6 @@ public class DbMigrationConfig {
this.modelSuffix = modelSuffix;
}
/**
* Return true if the rollback script should not be output.
*/
public boolean isSuppressRollback() {
return suppressRollback;
}
/**
* Set to true to suppress the output of the rollback script.
*/
public void setSuppressRollback(boolean suppressRollback) {
this.suppressRollback = suppressRollback;
}
/**
* Return the apply script suffix (defaults to sql).
*/
@@ -209,31 +146,31 @@ public class DbMigrationConfig {
}
/**
* Return the drop script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
* Return true if the generated file comment should be included.
*/
public String getDropSuffix() {
return dropSuffix;
public boolean isIncludeGeneratedFileComment() {
return includeGeneratedFileComment;
}
/**
* Set the drop script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
* Set to true if the generated file comment should be included.
*/
public void setDropSuffix(String dropSuffix) {
this.dropSuffix = dropSuffix;
public void setIncludeGeneratedFileComment(boolean includeGeneratedFileComment) {
this.includeGeneratedFileComment = includeGeneratedFileComment;
}
/**
* Return the rollback script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
* Return the migration version (or "next") to generate pending drops for.
*/
public String getRollbackSuffix() {
return rollbackSuffix;
public String getGeneratePendingDrop() {
return generatePendingDrop;
}
/**
* Set the rollback script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
* Set the migration version (or "next") to generate pending drops for.
*/
public void setRollbackSuffix(String rollbackSuffix) {
this.rollbackSuffix = rollbackSuffix;
public void setGeneratePendingDrop(String generatePendingDrop) {
this.generatePendingDrop = generatePendingDrop;
}
/**
@@ -259,8 +196,6 @@ public class DbMigrationConfig {
* into a single directory.
*/
public void singleDirectory() {
this.dropPath = "";
this.rollbackPath = "";
this.modelPath = "";
}
@@ -274,16 +209,13 @@ public class DbMigrationConfig {
singleDirectory();
} else {
modelPath = properties.get("migration.modelPath", modelPath);
rollbackPath = properties.get("migration.rollbackPath", rollbackPath);
dropPath = properties.get("migration.dropPath", dropPath);
}
applySuffix = properties.get("migration.applySuffix", applySuffix);
dropSuffix = properties.get("migration.dropSuffix", dropSuffix);
rollbackSuffix = properties.get("migration.rollbackSuffix", rollbackSuffix);
modelSuffix = properties.get("migration.modelSuffix", modelSuffix);
includeGeneratedFileComment = properties.getBoolean("migration.includeGeneratedFileComment", includeGeneratedFileComment);
generatePendingDrop = properties.get("migration.generatePendingDrop", generatePendingDrop);
platform = properties.getEnum(DbPlatformName.class, "migration.platform", platform);
suppressRollback = properties.getBoolean("migration.suppressRollback", suppressRollback);
generate = properties.getBoolean("migration.generate", generate);
version = properties.get("migration.version", version);
@@ -307,7 +239,6 @@ public class DbMigrationConfig {
return generate;
}
/**
* Called by EbeanServer on start.
*
@@ -0,0 +1,248 @@
package com.avaje.ebean.config;
import com.avaje.ebean.annotation.DocStoreMode;
/**
* Configuration for the Document store integration (e.g. ElasticSearch).
*/
public class DocStoreConfig {
/**
* True when the Document store integration is active/on.
*/
protected boolean active;
/**
* Set to true means Ebean will generate mapping files on startup.
*/
protected boolean generateMapping;
/**
* When true the Document store should drop and re-create document indexes.
*/
protected boolean dropCreate;
/**
* When true the Document store should create any document indexes that don't already exist.
*/
protected boolean create;
/**
* The URL of the Document store server. For example: http://localhost:9200.
*/
protected String url;
/**
* The default mode used by indexes.
*/
protected DocStoreMode persist = DocStoreMode.UPDATE;
/**
* The default batch size to use for the Bulk API calls.
*/
protected int bulkBatchSize = 1000;
/**
* Resource path for the Document store mapping files.
*/
protected String mappingPath;
/**
* Suffix used for mapping files.
*/
protected String mappingSuffix;
/**
* Location of resources that mapping files are generated into.
*/
protected String pathToResources = "src/main/resources";
/**
* Return true if the Document store (ElasticSearch) integration is active.
*/
public boolean isActive() {
String systemValue = System.getProperty("ebean.docstore.active");
if (systemValue != null) {
return Boolean.parseBoolean(systemValue);
}
return active;
}
/**
* Set to true to make the Document store (ElasticSearch) integration active.
*/
public void setActive(boolean active) {
this.active = active;
}
/**
* Return the URL to the Document store.
*/
public String getUrl() {
String systemValue = System.getProperty("ebean.docstore.url");
if (systemValue != null) {
return systemValue;
}
return url;
}
/**
* Set the URL to the Document store server.
* <p>
* For a local ElasticSearch server this would be: http://localhost:9200
*/
public void setUrl(String url) {
this.url = url;
}
/**
* Return true if Ebean should generate mapping files on server startup.
*/
public boolean isGenerateMapping() {
return generateMapping;
}
/**
* Set to true if Ebean should generate mapping files on server startup.
*/
public void setGenerateMapping(boolean generateMapping) {
this.generateMapping = generateMapping;
}
/**
* Return true if the document store should recreate mapped indexes.
*/
public boolean isDropCreate() {
return dropCreate;
}
/**
* Set to true if the document store should recreate mapped indexes.
*/
public void setDropCreate(boolean dropCreate) {
this.dropCreate = dropCreate;
}
/**
* Create true if the document store should create mapped indexes that don't yet exist.
* This is only used if dropCreate is false.
*/
public boolean isCreate() {
return create;
}
/**
* Set to true if the document store should create mapped indexes that don't yet exist.
* This is only used if dropCreate is false.
*/
public void setCreate(boolean create) {
this.create = create;
}
/**
* Return the default batch size to use for calls to the Bulk API.
*/
public int getBulkBatchSize() {
return bulkBatchSize;
}
/**
* Set the default batch size to use for calls to the Bulk API.
* <p>
* The batch size can be set on a transaction via {@link com.avaje.ebean.Transaction#setDocStoreBatchSize(int)}.
* </p>
*/
public void setBulkBatchSize(int bulkBatchSize) {
this.bulkBatchSize = bulkBatchSize;
}
/**
* Return the mapping path.
*/
public String getMappingPath() {
return mappingPath;
}
/**
* Set the mapping path.
*/
public void setMappingPath(String mappingPath) {
this.mappingPath = mappingPath;
}
/**
* Return the mapping suffix.
*/
public String getMappingSuffix() {
return mappingSuffix;
}
/**
* Set the mapping suffix.
*/
public void setMappingSuffix(String mappingSuffix) {
this.mappingSuffix = mappingSuffix;
}
/**
* Return the relative file system path to resources when generating mapping files.
*/
public String getPathToResources() {
return pathToResources;
}
/**
* Set the relative file system path to resources when generating mapping files.
*/
public void setPathToResources(String pathToResources) {
this.pathToResources = pathToResources;
}
/**
* Return the default behavior for when Insert, Update and Delete events occur on beans that have an associated
* Document store.
*/
public DocStoreMode getPersist() {
return persist;
}
/**
* Set the default behavior for when Insert, Update and Delete events occur on beans that have an associated
* Document store.
* <ul>
* <li>DocStoreEvent.UPDATE - build and send message to Bulk API</li>
* <li>DocStoreEvent.QUEUE - add an entry with the index type and id only into a queue for later processing</li>
* <li>DocStoreEvent.IGNORE - ignore. Most likely used when some scheduled batch job handles updating the index</li>
* </ul>
* <p>
* You might choose to use QUEUE if that particular index data is updating very frequently or the cost of indexing
* is expensive. Setting it to QUEUE can mean many changes can be batched together potentially coalescing multiple
* updates for an index entry into a single update.
* </p>
* <p>
* You might choose to use IGNORE when you have your own external process for updating the indexes. In this case
* you don't want Ebean to do anything when the data changes.
* </p>
*/
public void setPersist(DocStoreMode persist) {
this.persist = persist;
}
/**
* Load settings specified in properties files.
*/
public void loadSettings(PropertiesWrapper properties) {
active = properties.getBoolean("docstore.active", active);
url = properties.get("docstore.url", url);
persist = properties.getEnum(DocStoreMode.class, "docstore.persist", persist);
bulkBatchSize = properties.getInt("docstore.bulkBatchSize", bulkBatchSize);
generateMapping = properties.getBoolean("docstore.generateMapping", generateMapping);
dropCreate = properties.getBoolean("docstore.dropCreate", dropCreate);
create = properties.getBoolean("docstore.create", create);
mappingPath = properties.get("docstore.mappingPath", mappingPath);
mappingSuffix = properties.get("docstore.mappingSuffix", mappingSuffix);
pathToResources = properties.get("docstore.pathToResources", pathToResources);
}
}
@@ -25,8 +25,10 @@ import com.fasterxml.jackson.core.JsonFactory;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.ServiceLoader;
/**
* The configuration used for creating a EbeanServer.
@@ -129,6 +131,11 @@ public class ServerConfig {
*/
private String classPathReaderClassName;
/**
* Configuration for the ElasticSearch integration.
*/
private DocStoreConfig docStoreConfig = new DocStoreConfig();
/**
* This is used to populate @WhoCreated, @WhoModified and
* support other audit features (who executed a query etc).
@@ -283,7 +290,7 @@ public class ServerConfig {
private DbConstraintNaming constraintNaming = new DbConstraintNaming();
/**
* Behaviour of update to include on the change properties.
* Behaviour of update to include on the change properties.
*/
private boolean updateChangesOnly = true;
@@ -387,6 +394,8 @@ public class ServerConfig {
*/
private boolean expressionEqualsWithNullAsNoop;
private String jodaLocalTimeMode;
/**
* Construct a Server Configuration for programmatically creating an EbeanServer.
*/
@@ -1151,6 +1160,20 @@ public class ServerConfig {
this.namingConvention = namingConvention;
}
/**
* Return the configuration for the ElasticSearch integration.
*/
public DocStoreConfig getDocStoreConfig() {
return docStoreConfig;
}
/**
* Set the configuration for the ElasticSearch integration.
*/
public void setDocStoreConfig(DocStoreConfig docStoreConfig) {
this.docStoreConfig = docStoreConfig;
}
/**
* Return the constraint naming convention used in DDL generation.
*/
@@ -1640,6 +1663,20 @@ public class ServerConfig {
this.disableClasspathSearch = disableClasspathSearch;
}
/**
* Return the mode to use for Joda LocalTime support 'normal' or 'utc'.
*/
public String getJodaLocalTimeMode() {
return jodaLocalTimeMode;
}
/**
* Set the mode to use for Joda LocalTime support 'normal' or 'utc'.
*/
public void setJodaLocalTimeMode(String jodaLocalTimeMode) {
this.jodaLocalTimeMode = jodaLocalTimeMode;
}
/**
* Programmatically add classes (typically entities) that this server should
* use.
@@ -2118,6 +2155,23 @@ public class ServerConfig {
this.classLoadConfig = classLoadConfig;
}
/**
* Return the service loader using the classLoader defined in ClassLoadConfig.
*/
public <T> ServiceLoader<T> serviceLoad(Class<T> spiService) {
return ServiceLoader.load(spiService, classLoadConfig.getClassLoader());
}
/**
* Return the first service using the service loader (or null).
*/
public <T> T service(Class<T> spiService) {
ServiceLoader<T> load = serviceLoad(spiService);
Iterator<T> serviceInstances = load.iterator();
return serviceInstances.hasNext() ? serviceInstances.next() : null;
}
/**
* Load settings from ebean.properties.
*/
@@ -2196,7 +2250,14 @@ public class ServerConfig {
}
/**
* This is broken out for the same reason as above - preserve existing behaviour but let it be overridden.
* This is broken out to allow overridden behaviour.
*/
protected void loadDocStoreSettings(PropertiesWrapper p) {
docStoreConfig.loadSettings(p);
}
/**
* This is broken out to allow overridden behaviour.
*/
protected void loadAutoTuneSettings(PropertiesWrapper p) {
autoTuneConfig.loadSettings(p);
@@ -2223,6 +2284,11 @@ public class ServerConfig {
}
loadDataSourceSettings(p);
if (docStoreConfig == null) {
docStoreConfig = new DocStoreConfig();
}
loadDocStoreSettings(p);
explicitTransactionBeginMode = p.getBoolean("explicitTransactionBeginMode", explicitTransactionBeginMode);
autoCommitMode = p.getBoolean("autoCommitMode", autoCommitMode);
useJtaTransactionManager = p.getBoolean("useJtaTransactionManager", useJtaTransactionManager);
@@ -2286,6 +2352,7 @@ public class ServerConfig {
dbUuid = DbUuid.BINARY;
}
localTimeWithNanos = p.getBoolean("localTimeWithNanos", localTimeWithNanos);
jodaLocalTimeMode = p.get("jodaLocalTimeMode", jodaLocalTimeMode);
lazyLoadBatchSize = p.getInt("lazyLoadBatchSize", lazyLoadBatchSize);
queryBatchSize = p.getInt("queryBatchSize", queryBatchSize);
@@ -16,8 +16,6 @@ public class OraclePlatform extends DatabasePlatform {
this.name = "oracle";
this.maxTableNameLength = 30;
this.maxConstraintNameLength = 30;
// OnQueryOnly.CLOSE as a performance optimisation on Oracle
this.onQueryOnly = OnQueryOnly.CLOSE;
this.dbEncrypt = new OracleDbEncrypt();
this.sqlLimiter = new RownumSqlLimiter();
this.platformDdl = new Oracle10Ddl(this.dbTypeMap, this.dbIdentity);
@@ -19,9 +19,6 @@ public class PostgresPlatform extends DatabasePlatform {
public PostgresPlatform() {
super();
this.name = "postgres";
// OnQueryOnly.CLOSE as a performance optimisation on Postgres
this.onQueryOnly = OnQueryOnly.CLOSE;
this.likeClause = "like ? escape''";
this.selectCountWithAlias = true;
this.blobDbType = Types.LONGVARBINARY;
@@ -20,6 +20,7 @@ import com.avaje.ebean.dbmigration.migrationreader.MigrationXmlWriter;
import com.avaje.ebean.dbmigration.model.CurrentModel;
import com.avaje.ebean.dbmigration.model.MConfiguration;
import com.avaje.ebean.dbmigration.model.MigrationModel;
import com.avaje.ebean.dbmigration.model.MigrationVersion;
import com.avaje.ebean.dbmigration.model.ModelContainer;
import com.avaje.ebean.dbmigration.model.ModelDiff;
import com.avaje.ebean.dbmigration.model.PlatformDdlWriter;
@@ -59,6 +60,8 @@ public class DbMigration {
private static final String initialVersion = "1.0";
private static final String GENERATED_COMMENT = "THIS IS A GENERATED FILE - DO NOT MODIFY";
/**
* Set to true if DbMigration run with online EbeanServer instance.
*/
@@ -199,46 +202,15 @@ public class DbMigration {
if (!online) {
DbOffline.setRunningMigration();
}
setDefaults();
try {
Request request = createRequest();
File migrationDir = getMigrationDirectory();
File modelDir = getModelDirectory(migrationDir);
MigrationModel migrationModel = new MigrationModel(modelDir, migrationConfig.getModelSuffix());
ModelContainer migrated = migrationModel.read();
CurrentModel currentModel = new CurrentModel(server, constraintNaming);
ModelContainer current = currentModel.read();
ModelDiff diff = new ModelDiff(migrated);
diff.compareTo(current);
if (diff.isEmpty()) {
logger.info("no changes detected - no migration written");
return;
}
// there were actually changes to write
Migration dbMigration = diff.getMigration();
String fullVersion = getFullVersion(migrationModel);
logger.info("generating migration:{}", fullVersion);
if (!writeMigrationXml(dbMigration, modelDir, fullVersion)) {
logger.warn("migration already exists, not generating DDL");
String pendingVersion = generatePendingDrop();
if (pendingVersion != null) {
generatePendingDrop(request, pendingVersion);
} else {
if (databasePlatform != null) {
// writer needs the current model to provide table/column details for
// history ddl generation (triggers, history tables etc)
DdlWrite write = new DdlWrite(new MConfiguration(), currentModel.read());
PlatformDdlWriter writer = createDdlWriter(databasePlatform, "");
writer.processMigration(dbMigration, write, migrationDir , fullVersion);
}
writeExtraPlatformDdl(fullVersion, currentModel, dbMigration, migrationDir);
generateDiff(request);
}
} finally {
@@ -249,9 +221,128 @@ public class DbMigration {
}
/**
* Return the full version for the migration being generated.
* Generate the diff migration.
*/
private String getFullVersion(MigrationModel migrationModel) {
private void generateDiff(Request request) throws IOException {
List<String> pendingDrops = request.getPendingDrops();
if (!pendingDrops.isEmpty()) {
logger.info("Pending un-applied drops in versions {}", pendingDrops);
}
Migration migration = request.createDiffMigration();
if (migration == null) {
logger.info("no changes detected - no migration written");
} else {
// there were actually changes to write
generateMigration(request, migration, null);
}
}
/**
* Generate the migration based on the pendingDrops from a prior version.
*/
private void generatePendingDrop(Request request, String pendingVersion) throws IOException {
Migration migration = request.migrationForPendingDrop(pendingVersion);
generateMigration(request, migration, pendingVersion);
List<String> pendingDrops = request.getPendingDrops();
if (!pendingDrops.isEmpty()) {
logger.info("... remaining pending un-applied drops in versions {}", pendingDrops);
}
}
private Request createRequest() {
return new Request();
}
private class Request {
final File migrationDir;
final File modelDir;
final MigrationModel migrationModel;
final CurrentModel currentModel;
final ModelContainer migrated;
final ModelContainer current;
private Request() {
this.migrationDir = getMigrationDirectory();
this.modelDir = getModelDirectory(migrationDir);
this.migrationModel = new MigrationModel(modelDir, migrationConfig.getModelSuffix());
this.migrated = migrationModel.read();
this.currentModel = new CurrentModel(server, constraintNaming);
this.current = currentModel.read();
}
/**
* Return the migration for the pending drops for a given version.
*/
public Migration migrationForPendingDrop(String pendingVersion) {
Migration migration = migrated.migrationForPendingDrop(pendingVersion);
// register any remaining pending drops
migrated.registerPendingHistoryDropColumns(current);
return migration;
}
/**
* Return the list of versions that have pending un-applied drops.
*/
public List<String> getPendingDrops() {
return migrated.getPendingDrops();
}
/**
* Create and return the diff of the current model to the migration model.
*/
public Migration createDiffMigration() {
ModelDiff diff = new ModelDiff(migrated);
diff.compareTo(current);
return diff.isEmpty() ? null : diff.getMigration();
}
}
private void generateMigration(Request request, Migration dbMigration, String dropsFor) throws IOException {
String fullVersion = getFullVersion(request.migrationModel, dropsFor);
logger.info("generating migration:{}", fullVersion);
if (!writeMigrationXml(dbMigration, request.modelDir, fullVersion)) {
logger.warn("migration already exists, not generating DDL");
} else {
if (databasePlatform != null) {
// writer needs the current model to provide table/column details for
// history ddl generation (triggers, history tables etc)
DdlWrite write = new DdlWrite(new MConfiguration(), request.current);
PlatformDdlWriter writer = createDdlWriter(databasePlatform, "");
writer.processMigration(dbMigration, write, request.migrationDir , fullVersion);
}
writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir);
}
}
/**
* Return true if the next pending drop changeSet should be generated as the next migration.
*/
private String generatePendingDrop() {
String nextDrop = System.getProperty("ddl.migration.pendingDropsFor");
if (nextDrop != null) {
return nextDrop;
}
return migrationConfig.getGeneratePendingDrop();
}
/**
* Return the full version for the migration being generated.
*
* The full version can contain a comment suffix after a "__" double underscore.
*/
private String getFullVersion(MigrationModel migrationModel, String dropsFor) {
String version = migrationConfig.getVersion();
if (version == null) {
@@ -259,10 +350,14 @@ public class DbMigration {
}
String fullVersion = version;
if (migrationConfig.getName() != null) {
fullVersion += "__" + toUnderScore(migrationConfig.getName());
String name = migrationConfig.getName();
if (name != null) {
fullVersion += "__" + toUnderScore(name);
} else if (dropsFor != null) {
fullVersion += "__" + toUnderScore("dropsFor_" + MigrationVersion.trim(dropsFor));
} else if (version.equals(initialVersion)) {
fullVersion += "__initial";
}
return fullVersion;
}
@@ -304,7 +399,8 @@ public class DbMigration {
if (file.exists()) {
return false;
}
MigrationXmlWriter xmlWriter = new MigrationXmlWriter();
String comment = migrationConfig.isIncludeGeneratedFileComment() ? GENERATED_COMMENT : null;
MigrationXmlWriter xmlWriter = new MigrationXmlWriter(comment);
xmlWriter.write(dbMigration, file);
return true;
}
@@ -2,7 +2,6 @@ package com.avaje.ebean.dbmigration;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.dbmigration.model.CurrentModel;
import com.avaje.ebeaninternal.api.SpiEbeanPlugin;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import javax.persistence.PersistenceException;
@@ -30,8 +29,8 @@ public class DdlGenerator {
private final boolean createOnly;
private CurrentModel currentModel;
private String dropContent;
private String createContent;
private String dropAllContent;
private String createAllContent;
public DdlGenerator(SpiEbeanServer server, ServerConfig serverConfig) {
this.server = server;
@@ -84,18 +83,18 @@ public class DdlGenerator {
protected void runDropSql() throws IOException {
if (!createOnly) {
if (dropContent == null) {
dropContent = readFile(getDropFileName());
if (dropAllContent == null) {
dropAllContent = readFile(getDropFileName());
}
runScript(true, dropContent, getDropFileName());
runScript(true, dropAllContent, getDropFileName());
}
}
protected void runCreateSql() throws IOException {
if (createContent == null) {
createContent = readFile(getCreateFileName());
if (createAllContent == null) {
createAllContent = readFile(getCreateFileName());
}
runScript(false, createContent, getCreateFileName());
runScript(false, createAllContent, getCreateFileName());
}
protected void runInitSql() throws IOException {
@@ -132,8 +131,7 @@ public class DdlGenerator {
protected void writeDrop(String dropFile) {
try {
String c = generateDropDdl();
writeFile(dropFile, c);
writeFile(dropFile, generateDropAllDdl());
} catch (IOException e) {
throw new PersistenceException("Error generating Drop DDL", e);
}
@@ -142,28 +140,27 @@ public class DdlGenerator {
protected void writeCreate(String createFile) {
try {
String c = generateCreateDdl();
writeFile(createFile, c);
writeFile(createFile, generateCreateAllDdl());
} catch (IOException e) {
throw new PersistenceException("Error generating Create DDL", e);
}
}
protected String generateDropDdl() {
protected String generateDropAllDdl() {
try {
dropContent = currentModel().getDropDdl();
return dropContent;
dropAllContent = currentModel().getDropAllDdl();
return dropAllContent;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
protected String generateCreateDdl() {
protected String generateCreateAllDdl() {
try {
createContent = currentModel().getCreateDdl();
return createContent;
createAllContent = currentModel().getCreateDdl();
return createAllContent;
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -12,32 +12,17 @@ public class DdlWrite {
private final ModelContainer currentModel;
private final DdlBuffer applyDropDependencies;
private final DdlBuffer apply;
private final DdlBuffer applyForeignKeys;
private final DdlBuffer applyHistory;
private final DdlBuffer rollbackForeignKeys;
private final DdlBuffer dropAllForeignKeys;
private final DdlBuffer rollback;
/**
* For DDL that drops tables and columns etc.
*
* This DDL typically can not run automatically in production as there is most commonly
* existing servers running the application using these tables and columns. Typically
* these drop statements may be executed AFTER all the servers in the application have
* migrated onto new code.
*/
private final DdlBuffer drop;
/**
* For use when History is turned off for a base table or history is no longer
* desired on specific columns. This DDL should typically execute manually after review
* by DBA's.
*/
private final DdlBuffer dropHistory;
private final DdlBuffer dropAll;
/**
* Create without any configuration or current model (no history support).
@@ -51,13 +36,12 @@ public class DdlWrite {
*/
public DdlWrite(MConfiguration configuration, ModelContainer currentModel) {
this.currentModel = currentModel;
this.applyDropDependencies = new BaseDdlBuffer(configuration);
this.apply = new BaseDdlBuffer(configuration);
this.applyForeignKeys = new BaseDdlBuffer(configuration);
this.applyHistory = new BaseDdlBuffer(configuration);
this.rollbackForeignKeys = new BaseDdlBuffer(configuration);
this.rollback = new BaseDdlBuffer(configuration);
this.drop = new BaseDdlBuffer(configuration);
this.dropHistory = new BaseDdlBuffer(configuration);
this.dropAllForeignKeys = new BaseDdlBuffer(configuration);
this.dropAll = new BaseDdlBuffer(configuration);
}
/**
@@ -77,22 +61,8 @@ public class DdlWrite {
public boolean isApplyEmpty() {
return apply.getBuffer().isEmpty()
&& applyForeignKeys.getBuffer().isEmpty()
&& applyHistory.getBuffer().isEmpty();
}
/**
* Return true if the apply rollback buffers are all empty.
*/
public boolean isApplyRollbackEmpty() {
return rollback.getBuffer().isEmpty()
&& rollbackForeignKeys.getBuffer().isEmpty();
}
/**
* Return true the drop buffers are empty.
*/
public boolean isDropEmpty() {
return drop.getBuffer().isEmpty() && dropHistory.getBuffer().isEmpty();
&& applyHistory.getBuffer().isEmpty()
&& applyDropDependencies.getBuffer().isEmpty();
}
/**
@@ -102,6 +72,13 @@ public class DdlWrite {
return apply;
}
/**
* Return the buffer that executes early to drop dependencies like views etc.
*/
public DdlBuffer applyDropDependencies() {
return applyDropDependencies;
}
/**
* Return the buffer that APPLY DDL is written to for foreign keys and their associated indexes.
* <p>
@@ -120,41 +97,17 @@ public class DdlWrite {
}
/**
* Return the buffer that ROLLBACK DDL is written to for foreign keys and associated indexes.
* Return the buffer used for the 'drop all DDL' for dropping foreign keys and associated indexes.
*/
public DdlBuffer rollbackForeignKeys() {
return rollbackForeignKeys;
public DdlBuffer dropAllForeignKeys() {
return dropAllForeignKeys;
}
/**
* Return the buffer that ROLLBACK DDL is written to which is considered safe to run when
* apply changes fail to execute. This will reverse the apply changes typically dropping
* newly created tables, foreign keys etc.
* <p>
* When apply changes are made against DB's that support transactional DDL you could argue
* that these rollback statements are not necessary.
* <p>
* Note that statements added to this rollback buffer are executed after foreign key rollback
* has been executed.
* Return the buffer used for the 'drop all DDL' to drop tables, views and history triggers etc.
*/
public DdlBuffer rollback() {
return rollback;
public DdlBuffer dropAll() {
return dropAll;
}
/**
* Return the buffer that destructive changes are written to. This is typically drop table and
* drop column.
*/
public DdlBuffer drop() {
return drop;
}
/**
* Return the buffer that is used when history is no longer required on a table or specific columns.
*/
public DdlBuffer dropHistory() {
return dropHistory;
}
}
@@ -123,6 +123,9 @@ public class BaseTableDdl implements TableDdl {
// defined on the columns
writePrimaryKeyConstraint(apply, createTable.getPkName(), toColumnNames(pk));
}
if (platformDdl.isInlineForeignKeys()) {
writeInlineForeignKeys(writer, createTable);
}
apply.newLine().append(")");
addTableCommentInline(apply, createTable);
@@ -140,7 +143,7 @@ public class BaseTableDdl implements TableDdl {
// add drop table to the rollback buffer - do this before
// we drop the related sequence (if sequences are used)
dropTable(writer.rollback(), tableName);
dropTable(writer.dropAll(), tableName);
if (useSequence) {
String pkCol = pk.get(0).getName();
@@ -149,10 +152,11 @@ public class BaseTableDdl implements TableDdl {
// add blank line for a bit of whitespace between tables
apply.end();
writer.rollback().end();
writeAddForeignKeys(writer, createTable);
writer.dropAll().end();
if (!platformDdl.isInlineForeignKeys()) {
writeAddForeignKeys(writer, createTable);
}
}
/**
@@ -204,7 +208,7 @@ public class BaseTableDdl implements TableDdl {
.append(platformDdl.alterTableAddUniqueConstraint(tableName, uqName, columnNames))
.endOfStatement();
write.rollbackForeignKeys()
write.dropAllForeignKeys()
.append(platformDdl.dropIndex(uqName, tableName))
.endOfStatement();
}
@@ -225,7 +229,7 @@ public class BaseTableDdl implements TableDdl {
String createSeq = platformDdl.createSequence(seqName, initial, allocate);
if (createSeq != null) {
writer.apply().append(createSeq).newLine();
writer.rollback().append(platformDdl.dropSequence(seqName)).endOfStatement();
writer.dropAll().append(platformDdl.dropSequence(seqName)).endOfStatement();
}
}
@@ -235,6 +239,43 @@ public class BaseTableDdl implements TableDdl {
platformDdl.createWithHistory(writer, table);
}
protected void writeInlineForeignKeys(DdlWrite write, CreateTable createTable) throws IOException {
for (Column column : createTable.getColumn()) {
String references = column.getReferences();
if (hasValue(references)) {
writeInlineForeignKey(write, column);
}
}
writeInlineCompoundForeignKeys(write, createTable);
}
protected void writeInlineForeignKey(DdlWrite write, Column column) throws IOException {
String references = column.getReferences();
int pos = references.lastIndexOf('.');
if (pos == -1) {
throw new IllegalStateException("Expecting period '.' character for table.column split but not found in [" + references + "]");
}
String refTableName = references.substring(0, pos);
String refColumnName = references.substring(pos + 1);
String fkConstraint = platformDdl.tableInlineForeignKey(new String[]{column.getName()}, refTableName, new String[]{refColumnName});
write.apply().append(",").newLine().append(" ").append(fkConstraint);
}
protected void writeInlineCompoundForeignKeys(DdlWrite write, CreateTable createTable) throws IOException {
List<ForeignKey> foreignKey = createTable.getForeignKey();
for (ForeignKey key : foreignKey) {
String refTableName = key.getRefTableName();
String[] cols = toColumnNamesSplit(key.getColumnNames());
String[] refColumns = toColumnNamesSplit(key.getRefColumnNames());
String fkConstraint = platformDdl.tableInlineForeignKey(cols, refTableName, refColumns);
write.apply().append(",").newLine().append(" ").append(fkConstraint);
}
}
protected void writeAddForeignKeys(DdlWrite write, CreateTable createTable) throws IOException {
String tableName = createTable.getName();
@@ -295,21 +336,24 @@ public class BaseTableDdl implements TableDdl {
fkeyBuffer.end();
write.rollbackForeignKeys()
write.dropAllForeignKeys()
.append(platformDdl.alterTableDropForeignKey(tableName, fkName)).endOfStatement();
if (indexName != null) {
write.rollbackForeignKeys()
write.dropAllForeignKeys()
.append(platformDdl.dropIndex(indexName, tableName)).endOfStatement();
}
write.rollbackForeignKeys().end();
write.dropAllForeignKeys().end();
}
protected void alterTableAddForeignKey(DdlBuffer buffer, String fkName, String tableName, String[] columns, String refTable, String[] refColumns) throws IOException {
buffer.append(platformDdl.alterTableAddForeignKey(tableName, fkName, columns, refTable, refColumns)).endOfStatement();
String fkConstraint = platformDdl.alterTableAddForeignKey(tableName, fkName, columns, refTable, refColumns);
if (fkConstraint != null && !fkConstraint.isEmpty()) {
buffer.append(fkConstraint).endOfStatement();
}
}
protected void appendColumns(String[] columns, DdlBuffer buffer) throws IOException {
@@ -471,7 +515,7 @@ public class BaseTableDdl implements TableDdl {
.append(platformDdl.createIndex(createIndex.getIndexName(), createIndex.getTableName(), cols))
.endOfStatement();
writer.rollback()
writer.dropAll()
.append(platformDdl.dropIndex(createIndex.getIndexName(), createIndex.getTableName()))
.endOfStatement();
}
@@ -520,7 +564,6 @@ public class BaseTableDdl implements TableDdl {
List<Column> columns = addColumn.getColumn();
for (Column column : columns) {
alterTableAddColumn(writer.apply(), tableName, column, false);
alterTableDropColumn(writer.rollback(), tableName, column.getName());
}
if (isTrue(addColumn.isWithHistory())) {
@@ -529,13 +572,11 @@ public class BaseTableDdl implements TableDdl {
for (Column column : columns) {
regenerateHistoryTriggers(tableName, HistoryTableUpdate.Change.ADD, column.getName());
alterTableAddColumn(writer.apply(), historyTable, column, true);
alterTableDropColumn(writer.rollback(), historyTable, column.getName());
}
}
// add a bit of whitespace
writer.apply().end();
writer.rollback().end();
}
/**
@@ -544,7 +585,7 @@ public class BaseTableDdl implements TableDdl {
@Override
public void generate(DdlWrite writer, DropTable dropTable) throws IOException {
dropTable(writer.drop(), dropTable.getName());
dropTable(writer.apply(), dropTable.getName());
}
/**
@@ -555,14 +596,14 @@ public class BaseTableDdl implements TableDdl {
String tableName = dropColumn.getTableName();
alterTableDropColumn(writer.drop(), tableName, dropColumn.getColumnName());
alterTableDropColumn(writer.apply(), tableName, dropColumn.getColumnName());
if (isTrue(dropColumn.isWithHistory())) {
// also drop from the history table
regenerateHistoryTriggers(tableName, HistoryTableUpdate.Change.DROP, dropColumn.getColumnName());
alterTableDropColumn(writer.drop(), historyTable(tableName), dropColumn.getColumnName());
alterTableDropColumn(writer.apply(), historyTable(tableName), dropColumn.getColumnName());
}
writer.drop().end();
writer.apply().end();
}
/**
@@ -642,21 +683,6 @@ public class BaseTableDdl implements TableDdl {
if (hasValue(ddl)) {
writer.apply().append(ddl).endOfStatement();
// reverse and generate the rollback statement
String currentType = alter.getCurrentType();
String type = alter.getType();
Boolean currentNotnull = alter.isCurrentNotnull();
Boolean notnull = alter.isNotnull();
alter.setCurrentType(type);
alter.setType(currentType);
alter.setNotnull(currentNotnull);
alter.setCurrentNotnull(notnull);
// write the rollback
ddl = platformDdl.alterColumnBaseAttributes(alter);
writer.rollback().append(ddl).endOfStatement();
if (isTrue(alter.isWithHistory()) && alter.getType() != null) {
// mysql and sql server column type change allowing nulls in the history table column
AlterColumn alterHistoryColumn = new AlterColumn();
@@ -667,11 +693,6 @@ public class BaseTableDdl implements TableDdl {
// write the apply to history table
writer.apply().append(histColumnDdl).endOfStatement();
// write the rollback from history table
alterHistoryColumn.setType(currentType);
histColumnDdl = platformDdl.alterColumnBaseAttributes(alterHistoryColumn);
writer.rollback().append(histColumnDdl).endOfStatement();
}
}
}
@@ -689,8 +710,6 @@ public class BaseTableDdl implements TableDdl {
String ddl = platformDdl.alterColumnNotnull(alter.getTableName(), alter.getColumnName(), alter.isNotnull());
if (hasValue(ddl)) {
writer.apply().append(ddl).endOfStatement();
ddl = platformDdl.alterColumnNotnull(alter.getTableName(), alter.getColumnName(), alter.isCurrentNotnull());
writer.rollback().append(ddl).endOfStatement();
}
}
@@ -699,14 +718,10 @@ public class BaseTableDdl implements TableDdl {
String ddl = platformDdl.alterColumnType(alter.getTableName(), alter.getColumnName(), alter.getType());
if (hasValue(ddl)) {
writer.apply().append(ddl).endOfStatement();
ddl = platformDdl.alterColumnType(alter.getTableName(), alter.getColumnName(), alter.getCurrentType());
writer.rollback().append(ddl).endOfStatement();
if (isTrue(alter.isWithHistory())) {
// apply same type change to matching column in the history table
ddl = platformDdl.alterColumnType(historyTable(alter.getTableName()), alter.getColumnName(), alter.getType());
writer.apply().append(ddl).endOfStatement();
ddl = platformDdl.alterColumnType(historyTable(alter.getTableName()), alter.getColumnName(), alter.getCurrentType());
writer.rollback().append(ddl).endOfStatement();
}
}
}
@@ -762,7 +777,7 @@ public class BaseTableDdl implements TableDdl {
.append(platformDdl.alterTableAddUniqueConstraint(alter.getTableName(), uqName, cols))
.endOfStatement();
writer.rollbackForeignKeys()
writer.dropAllForeignKeys()
.append(platformDdl.dropIndex(uqName, alter.getTableName()))
.endOfStatement();
}
@@ -10,7 +10,6 @@ import com.avaje.ebean.dbmigration.model.MColumn;
import com.avaje.ebean.dbmigration.model.MTable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -45,21 +44,43 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
this.historySuffix = serverConfig.getHistoryTableSuffix();
this.constraintNaming = serverConfig.getConstraintNaming();
this.sysPeriodStart = sysPeriod+"_start";
this.sysPeriodEnd = sysPeriod+"_end";
this.sysPeriodStart = sysPeriod + "_start";
this.sysPeriodEnd = sysPeriod + "_end";
}
@Override
public void regenerateHistoryTriggers(DdlWrite writer, HistoryTableUpdate update) throws IOException {
public void updateTriggers(DdlWrite writer, HistoryTableUpdate update) throws IOException {
MTable table = writer.getTable(update.getBaseTable());
if (table == null) {
throw new IllegalStateException("MTable "+update.getBaseTable()+" not found in writer? (required for history DDL)");
throw new IllegalStateException("MTable " + update.getBaseTable() + " not found in writer? (required for history DDL)");
}
regenerateHistoryTriggers(writer, table, update);
updateTriggers(writer, table, update);
}
protected abstract void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException;
/**
* Replace the existing triggers/stored procedures/views for history table support given the included columns.
*/
protected abstract void updateHistoryTriggers(DbTriggerUpdate triggerUpdate) throws IOException;
/**
* Process the HistoryTableUpdate which can result in changes to the apply, rollback
* and drop scripts.
*/
protected void updateTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
writer.applyHistory().append("-- changes: ").append(update.description()).newLine();
updateHistoryTriggers(createDbTriggerUpdate(writer, table));
}
protected DbTriggerUpdate createDbTriggerUpdate(DdlWrite writer, MTable table) {
List<String> columns = columnNamesForApply(table);
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
return new DbTriggerUpdate(baseTableName, historyTableName, writer, columns);
}
@Override
public void dropHistoryTable(DdlWrite writer, DropHistoryTable dropHistoryTable) throws IOException {
@@ -67,18 +88,17 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
String baseTable = dropHistoryTable.getBaseTable();
// drop in appropriate order
dropTriggers(writer.dropHistory(), baseTable);
dropHistoryTableEtc(writer.dropHistory(), baseTable);
dropTriggers(writer.applyDropDependencies(), baseTable);
dropHistoryTableEtc(writer.applyDropDependencies(), baseTable);
}
@Override
public void addHistoryTable(DdlWrite writer, AddHistoryTable addHistoryTable) throws IOException {
String baseTable = addHistoryTable.getBaseTable();
MTable table = writer.getTable(baseTable);
if (table == null) {
throw new IllegalStateException("MTable "+baseTable+" not found in writer? (required for history DDL)");
throw new IllegalStateException("MTable " + baseTable + " not found in writer? (required for history DDL)");
}
createWithHistory(writer, table);
@@ -90,12 +110,11 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
String baseTable = table.getName();
String whenCreatedColumn = table.getWhenCreatedColumn();
// rollback changes in appropriate order
dropTriggers(writer.rollback(), baseTable);
dropHistoryTableEtc(writer.rollback(), baseTable);
dropTriggers(writer.dropAll(), baseTable);
dropHistoryTableEtc(writer.dropAll(), baseTable);
addHistoryTable(writer, table, whenCreatedColumn);
addStoredFunction(writer, table, null);
createStoredFunction(writer, table);
createTriggers(writer, table);
}
@@ -103,7 +122,7 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
protected abstract void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException;
protected void addStoredFunction(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
protected void createStoredFunction(DdlWrite writer, MTable table) throws IOException {
// do nothing
}
@@ -158,7 +177,7 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
apply.append("create table ").append(table.getName()).append(historySuffix).append("(").newLine();
Collection<MColumn> cols = table.getColumns().values();
Collection<MColumn> cols = table.allColumns();
for (MColumn column : cols) {
if (!column.isDraftOnly()) {
writeColumnDefinition(apply, column.getName(), column.getType());
@@ -191,6 +210,26 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
.endOfStatement().end();
}
/**
* Create or replace the with_history view with explicit columns.
*/
protected void createWithHistoryView(DbTriggerUpdate update) throws IOException {
DdlBuffer apply = update.historyBuffer();
apply.append("create or replace view ").append(update.getBaseTable()).append(viewSuffix).append(" as select ");
appendColumnNames(apply, update.getColumns(), "");
appendSysPeriodColumns(apply, ", ");
apply.append(" from ").append(update.getBaseTable()).append(" union all select ");
appendColumnNames(apply, update.getColumns(), "");
appendSysPeriodColumns(apply, ", ");
apply.append(" from ").append(update.getHistoryTable()).endOfStatement().end();
}
protected void appendSysPeriodColumns(DdlBuffer apply, String prefix) throws IOException {
appendColumnName(apply, prefix, sysPeriodStart);
appendColumnName(apply, prefix, sysPeriodEnd);
}
protected void dropHistoryTableEtc(DdlBuffer buffer, String baseTableName) throws IOException {
buffer.append("drop view ").append(baseTableName).append(viewSuffix).endOfStatement();
@@ -203,8 +242,6 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
buffer.append("alter table ").append(baseTableName).append(" drop column ").append(sysPeriodEnd).endOfStatement();
}
//protected abstract void addFunction(DdlBuffer apply, String procedureName, String historyTable, List<String> includedColumns) throws IOException;
protected void appendInsertIntoHistory(DdlBuffer buffer, String historyTable, List<String> columns) throws IOException {
buffer.append(" insert into ").append(historyTable).append(" (").append(sysPeriodStart).append(",").append(sysPeriodEnd).append(",");
@@ -216,7 +253,7 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
protected void appendColumnNames(DdlBuffer buffer, List<String> columns, String columnPrefix) throws IOException {
for (int i=0; i< columns.size(); i++) {
for (int i = 0; i < columns.size(); i++) {
if (i > 0) {
buffer.append(", ");
}
@@ -226,18 +263,26 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
}
/**
* Return the list of included columns in order.
* Append a single column to the buffer if it is not null.
*/
protected List<String> includedColumnNames(MTable table) throws IOException {
protected void appendColumnName(DdlBuffer buffer, String prefix, String columnName) throws IOException {
Collection<MColumn> columns = table.getColumns().values();
List<String> includedColumns = new ArrayList<String>(columns.size());
for (MColumn column : columns) {
if (column.isIncludeInHistory()) {
includedColumns.add(column.getName());
}
if (columnName != null) {
buffer.append(prefix).append(columnName);
}
return includedColumns;
}
/**
* Return the column names included in history for the apply script.
* <p>
* Note that dropped columns are actually still included at this point as they are going
* to be removed from the history handling when the drop script runs that also deletes
* the column.
* </p>
*/
protected List<String> columnNamesForApply(MTable table) {
return table.allHistoryColumns(true);
}
}
@@ -0,0 +1,63 @@
package com.avaje.ebean.dbmigration.ddlgeneration.platform;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
import java.util.List;
/**
* DB trigger update when a change occurs on a table with history.
*/
public class DbTriggerUpdate {
private final String baseTableName;
private final String historyTableName;
private final DdlWrite writer;
private final List<String> columns;
public DbTriggerUpdate(String baseTableName, String historyTableName, DdlWrite writer, List<String> columns) {
this.baseTableName = baseTableName;
this.historyTableName = historyTableName;
this.writer = writer;
this.columns = columns;
}
/**
* Return the appropriate buffer for the current mode.
*/
public DdlBuffer historyBuffer() {
return writer.applyHistory();
}
/**
* Return the appropriate drop dependency buffer for the current mode.
*/
public DdlBuffer dropDependencyBuffer() {
return writer.applyDropDependencies();
}
/**
* Return the base table name.
*/
public String getBaseTable() {
return baseTableName;
}
/**
* Return the history table name.
*/
public String getHistoryTable() {
return historyTableName;
}
/**
* Return the included columns.
*/
public List<String> getColumns() {
return columns;
}
}
@@ -6,7 +6,6 @@ import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
import com.avaje.ebean.dbmigration.model.MTable;
import java.io.IOException;
import java.util.List;
/**
* H2 history support using DB triggers to maintain a history table.
@@ -28,40 +27,20 @@ public class H2HistoryDdl extends DbTriggerBasedHistoryDdl {
protected void createTriggers(DdlWrite writer, MTable table) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DdlBuffer apply = writer.applyHistory();
addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName);
}
@Override
protected void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
protected void updateHistoryTriggers(DbTriggerUpdate update) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DdlBuffer apply = writer.applyHistory();
apply.append("-- Regenerated ").newLine();
apply.append("-- changes: ").append(update.description()).newLine();
dropTriggers(apply, baseTableName);
addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
// put a reverted version into the rollback buffer
update.toRevertedColumns(includedColumns);
DdlBuffer rollback = writer.rollback();
rollback.append("-- Revert regenerated ").newLine();
rollback.append("-- revert changes: ").append(update.description()).newLine();
dropTriggers(rollback, baseTableName);
addCreateTrigger(rollback, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
DdlBuffer buffer = update.historyBuffer();
dropTriggers(buffer, update.getBaseTable());
addCreateTrigger(buffer, updateTriggerName(update.getBaseTable()), update.getBaseTable());
}
private void addCreateTrigger(DdlBuffer apply, String triggerName, String baseTable, String historyTable, List<String> includedColumns) throws IOException {
private void addCreateTrigger(DdlBuffer apply, String triggerName, String baseTable) throws IOException {
// Note that this does not take into account the historyTable name (excepts _history suffix) and
// does not take into account excluded columns (all columns included in history)
@@ -10,7 +10,6 @@ import java.util.List;
*/
public class HistoryTableUpdate {
/**
* Column change type.
*/
@@ -21,35 +20,25 @@ public class HistoryTableUpdate {
EXCLUDE
}
public static class Column {
private static class Column {
final Change change;
final String column;
public final Change change;
public final String column;
public Column(Change change, String column) {
this.change = change;
this.column = column;
}
public String toString() {
return description();
}
public String description() {
return change.name().toLowerCase()+" "+column;
}
public void apply(List<String> includedColumns) {
switch (change) {
case ADD:
case INCLUDE: {
includedColumns.remove(column);
break;
}
case DROP:
case EXCLUDE: {
includedColumns.add(column);
break;
}
default:
throw new IllegalStateException("Unexpected change "+change);
}
}
}
private final String baseTable;
@@ -65,27 +54,12 @@ public class HistoryTableUpdate {
/**
* Return a description of the changes that cause the history trigger/function
* to be regenerated (added, dropped, included or excluded columns).
* to be regenerated (added, included, excluded and dropped columns).
*/
public String description() {
StringBuilder sb = new StringBuilder(90);
for (int i = 0; i < columnChanges.size(); i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(columnChanges.get(i).description());
}
return sb.toString();
return columnChanges.toString();
}
public void toRevertedColumns(List<String> includedColumns) {
for (Column columnChange : columnChanges) {
columnChange.apply(includedColumns);
}
}
/**
* Add a comment for column added, dropped, included or excluded.
*/
@@ -100,10 +74,4 @@ public class HistoryTableUpdate {
return baseTable;
}
/**
* Return the comments.
*/
public List<Column> getColumnChanges() {
return columnChanges;
}
}
@@ -5,7 +5,6 @@ import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
import com.avaje.ebean.dbmigration.model.MTable;
import java.io.IOException;
import java.util.List;
/**
* MySql history support using DB triggers to maintain a history table.
@@ -26,67 +25,46 @@ public class MySqlHistoryDdl extends DbTriggerBasedHistoryDdl {
@Override
protected void createTriggers(DdlWrite writer, MTable table) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DbTriggerUpdate update = createDbTriggerUpdate(writer, table);
DdlBuffer apply = writer.applyHistory();
addBeforeUpdate(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
addBeforeDelete(apply, deleteTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
addBeforeUpdate(updateTriggerName(update.getBaseTable()), update);
addBeforeDelete(deleteTriggerName(update.getBaseTable()), update);
}
@Override
protected void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
protected void updateHistoryTriggers(DbTriggerUpdate update) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DdlBuffer buffer = update.historyBuffer();
String baseTable = update.getBaseTable();
DdlBuffer apply = writer.applyHistory();
apply.append("-- Regenerated ").newLine();
apply.append("-- changes: ").append(update.description()).newLine();
// lock the base table while we drop and recreate the triggers
apply.append("lock tables ").append(baseTableName).append(" write").endOfStatement();
dropTriggers(apply, baseTableName);
addBeforeUpdate(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
addBeforeDelete(apply, deleteTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
apply.append("unlock tables").endOfStatement();
// put a reverted version into the rollback buffer
update.toRevertedColumns(includedColumns);
DdlBuffer rollback = writer.rollback();
rollback.append("-- Revert regenerated ").newLine();
rollback.append("-- revert changes: ").append(update.description()).newLine();
// lock the base table while we drop and recreate the triggers
rollback.append("lock tables ").append(baseTableName).append(" write").endOfStatement();
dropTriggers(rollback, baseTableName);
addBeforeUpdate(rollback, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
addBeforeDelete(rollback, deleteTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
rollback.append("unlock tables").endOfStatement();
buffer.append("lock tables ").append(baseTable).append(" write").endOfStatement();
dropTriggers(buffer, baseTable);
addBeforeUpdate(updateTriggerName(baseTable), update);
addBeforeDelete(deleteTriggerName(baseTable), update);
buffer.append("unlock tables").endOfStatement();
}
private void addBeforeUpdate(DdlBuffer apply, String triggerName, String baseTable, String historyTable, List<String> includedColumns) throws IOException {
private void addBeforeUpdate(String triggerName, DbTriggerUpdate update) throws IOException {
DdlBuffer apply = update.historyBuffer();
apply
.append("delimiter $$").newLine()
.append("create trigger ").append(triggerName).append(" before update on ").append(baseTable)
.append("create trigger ").append(triggerName).append(" before update on ").append(update.getBaseTable())
.append(" for each row begin").newLine();
appendInsertIntoHistory(apply, historyTable, includedColumns);
appendInsertIntoHistory(apply, update.getHistoryTable(), update.getColumns());
apply
.append(" set NEW.").append(sysPeriod).append("_start = now(6)").endOfStatement()
.append("end$$").newLine();
}
private void addBeforeDelete(DdlBuffer apply, String triggerName, String baseTable, String historyTable, List<String> includedColumns) throws IOException {
private void addBeforeDelete(String triggerName, DbTriggerUpdate update) throws IOException {
DdlBuffer apply = update.historyBuffer();
apply
.append("delimiter $$").newLine()
.append("create trigger ").append(triggerName).append(" before delete on ").append(baseTable)
.append("create trigger ").append(triggerName).append(" before delete on ").append(update.getBaseTable())
.append(" for each row begin").newLine();
appendInsertIntoHistory(apply, historyTable, includedColumns);
appendInsertIntoHistory(apply, update.getHistoryTable(), update.getColumns());
apply.append("end$$").newLine();
}
@@ -35,7 +35,7 @@ public class NoHistorySupportDdl implements PlatformHistoryDdl {
}
@Override
public void regenerateHistoryTriggers(DdlWrite write, HistoryTableUpdate update) {
public void updateTriggers(DdlWrite write, HistoryTableUpdate update) {
// does nothing
}
}
@@ -85,6 +85,11 @@ public class PlatformDdl {
protected DbConstraintNaming naming;
/**
* Generally not desired as then they are not named (used with SQLite).
*/
protected boolean inlineForeignKeys;
public PlatformDdl(DbTypeMap platformTypes, DbIdentity dbIdentity) {
this.dbIdentity = dbIdentity;
this.typeConverter = new PlatformTypeConverter(platformTypes);
@@ -128,6 +133,14 @@ public class PlatformDdl {
return inlineComments;
}
/**
* Return true if foreign key reference constraints need to inlined with create table.
* Ideally we don't do this as then the constraints are not named. Do this for SQLite.
*/
public boolean isInlineForeignKeys() {
return inlineForeignKeys;
}
/**
* Write all the table columns converting to platform types as necessary.
*/
@@ -200,7 +213,7 @@ public class PlatformDdl {
* Regenerate the history triggers (or function) due to a column being added/dropped/excluded or included.
*/
public void regenerateHistoryTriggers(DdlWrite write, HistoryTableUpdate update) throws IOException {
historyDdl.regenerateHistoryTriggers(write, update);
historyDdl.updateTriggers(write, update);
}
/**
@@ -255,6 +268,20 @@ public class PlatformDdl {
return buffer.toString();
}
/**
* Return the foreign key constraint when used inline with create table.
*/
public String tableInlineForeignKey(String[] columns, String refTable, String[] refColumns) {
StringBuilder buffer = new StringBuilder(90);
buffer.append("foreign key");
appendColumns(columns, buffer);
buffer.append(" references ").append(lowerTableName(refTable));
appendColumns(refColumns, buffer);
appendWithSpace(foreignKeyRestrict, buffer);
return buffer.toString();
}
/**
* Add foreign key.
*/
@@ -36,5 +36,5 @@ public interface PlatformHistoryDdl {
/**
* Regenerate the history triggers/stored function due to column added/dropped/included or excluded.
*/
void regenerateHistoryTriggers(DdlWrite write, HistoryTableUpdate baseTable) throws IOException;
void updateTriggers(DdlWrite write, HistoryTableUpdate baseTable) throws IOException;
}
@@ -14,7 +14,7 @@ public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
public PostgresHistoryDdl() {
this.currentTimestamp = "statement_timestamp()";
this.currentTimestamp = "current_timestamp";
}
/**
@@ -45,6 +45,11 @@ public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
}
}
@Override
protected void appendSysPeriodColumns(DdlBuffer apply, String prefix) throws IOException {
appendColumnName(apply, prefix, sysPeriod);
}
@Override
protected void dropSysPeriodColumns(DdlBuffer buffer, String baseTableName) throws IOException {
buffer.append("alter table ").append(baseTableName).append(" drop column ").append(sysPeriod).endOfStatement();
@@ -72,7 +77,7 @@ public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
buffer.end();
}
protected void addFunction(DdlBuffer apply, String procedureName, String historyTable, List<String> includedColumns) throws IOException {
protected void createOrReplaceFunction(DdlBuffer apply, String procedureName, String historyTable, List<String> includedColumns) throws IOException {
apply
.append("create or replace function ").append(procedureName).append("() returns trigger as $$").newLine()
.append("begin").newLine();
@@ -96,40 +101,36 @@ public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
}
@Override
protected void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
// just replace the stored function with 'create or replace'
addStoredFunction(writer, table, update);
}
@Override
protected void addStoredFunction(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
protected void createStoredFunction(DdlWrite writer, MTable table) throws IOException {
String procedureName = procedureName(table.getName());
String historyTable = historyTableName(table.getName());
List<String> includedColumns = includedColumnNames(table);
List<String> columnNames = columnNamesForApply(table);
createOrReplaceFunction(writer.applyHistory(), procedureName, historyTable, columnNames);
}
DdlBuffer apply = writer.applyHistory();
@Override
protected void updateHistoryTriggers(DbTriggerUpdate update) throws IOException {
if (update != null) {
apply.append("-- Regenerated ").append(procedureName).newLine();
apply.append("-- changes: ").append(update.description()).newLine();
}
String procedureName = procedureName(update.getBaseTable());
addFunction(apply, procedureName, historyTable, includedColumns);
recreateHistoryView(update);
createOrReplaceFunction(update.historyBuffer(), procedureName, update.getHistoryTable(), update.getColumns());
}
/**
* For postgres we need to drop and recreate the view. Well, we could add columns to the end of the view
* but otherwise we need to drop and create it.
*/
private void recreateHistoryView(DbTriggerUpdate update) throws IOException {
if (update != null) {
// put a reverted version into the rollback buffer
update.toRevertedColumns(includedColumns);
DdlBuffer buffer = update.dropDependencyBuffer();
// we need to drop the view early/first before any changes to the tables etc
buffer.append("drop view if exists ").append(update.getBaseTable()).append(viewSuffix).endOfStatement();
DdlBuffer rollback = writer.rollback();
rollback.append("-- Revert regenerated ").append(procedureName).newLine();
rollback.append("-- revert changes: ").append(update.description()).newLine();
addFunction(rollback, procedureName, historyTable, includedColumns);
}
// recreate the view with specific columns specified (the columns generally are not dropped until later)
createWithHistoryView(update);
}
@Override
@@ -2,6 +2,9 @@ package com.avaje.ebean.dbmigration.ddlgeneration.platform;
import com.avaje.ebean.config.dbplatform.DbIdentity;
import com.avaje.ebean.config.dbplatform.DbTypeMap;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
import java.io.IOException;
/**
* DB2 platform specific DDL.
@@ -10,7 +13,23 @@ public class SQLiteDdl extends PlatformDdl {
public SQLiteDdl(DbTypeMap platformTypes, DbIdentity dbIdentity) {
super(platformTypes, dbIdentity);
this.identitySuffix = " autoincrement";
this.identitySuffix = "";
this.inlineForeignKeys = true;
}
@Override
public void addTableComment(DdlBuffer apply, String tableName, String tableComment) throws IOException {
// not supported
}
@Override
public void addColumnComment(DdlBuffer apply, String table, String column, String comment) throws IOException {
// not supported
}
@Override
public String alterTableAddForeignKey(String tableName, String fkName, String[] columns, String refTable, String[] refColumns) {
// not supported
return null;
}
}
@@ -5,7 +5,7 @@ import com.avaje.ebean.config.dbplatform.DbTypeMap;
/**
* Converts a logical column definition into platform specific one.
*
* <p>
* This translates standard sql types into platform specific ones.
*/
public class PlatformTypeConverter {
@@ -44,20 +44,21 @@ public class PlatformTypeConverter {
return columnDefinition;
}
String type = columnDefinition.substring(0,open);
String suffix = close + 1 < columnDefinition.length() ? columnDefinition.substring(close + 1) : "";
String type = columnDefinition.substring(0, open);
try {
DbType dbType = platformTypes.lookup(type);
int comma = columnDefinition.indexOf(',',open);
int comma = columnDefinition.indexOf(',', open);
if (comma > -1) {
// scale and precision - decimal(10,4)
int scale = Integer.parseInt(columnDefinition.substring(open+1, comma));
int precision = Integer.parseInt(columnDefinition.substring(comma+1, close));
return dbType.renderType(scale,precision);
int scale = Integer.parseInt(columnDefinition.substring(open + 1, comma));
int precision = Integer.parseInt(columnDefinition.substring(comma + 1, close));
return dbType.renderType(scale, precision) + suffix;
} else {
// scale - varchar(10)
int scale = Integer.parseInt(columnDefinition.substring(open+1, close));
return dbType.renderType(scale,0);
int scale = Integer.parseInt(columnDefinition.substring(open + 1, close));
return dbType.renderType(scale, 0) + suffix;
}
} catch (IllegalArgumentException e) {
@@ -73,7 +74,7 @@ public class PlatformTypeConverter {
try {
DbType dbType = platformTypes.lookup(columnDefinition);
return dbType.renderType(0,0);
return dbType.renderType(0, 0);
} catch (IllegalArgumentException e) {
// assume already platform specific, leave as is
@@ -27,6 +27,8 @@ import javax.xml.bind.annotation.XmlType;
* &lt;/choice>
* &lt;/sequence>
* &lt;attribute name="type" use="required" type="{http://ebean-orm.github.io/xml/ns/dbmigration}changeSetType" />
* &lt;attribute name="dropsFor" type="{http://www.w3.org/2001/XMLSchema}string" />
* &lt;attribute name="suppressDropsForever" type="{http://www.w3.org/2001/XMLSchema}boolean" />
* &lt;attribute name="generated" type="{http://www.w3.org/2001/XMLSchema}boolean" />
* &lt;attribute name="author" type="{http://www.w3.org/2001/XMLSchema}string" />
* &lt;attribute name="comment" type="{http://www.w3.org/2001/XMLSchema}string" />
@@ -62,6 +64,10 @@ public class ChangeSet {
protected List<Object> changeSetChildren;
@XmlAttribute(name = "type", required = true)
protected ChangeSetType type;
@XmlAttribute(name = "dropsFor")
protected String dropsFor;
@XmlAttribute(name = "suppressDropsForever")
protected Boolean suppressDropsForever;
@XmlAttribute(name = "generated")
protected Boolean generated;
@XmlAttribute(name = "author")
@@ -134,6 +140,54 @@ public class ChangeSet {
this.type = value;
}
/**
* Gets the value of the dropsFor property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getDropsFor() {
return dropsFor;
}
/**
* Sets the value of the dropsFor property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setDropsFor(String value) {
this.dropsFor = value;
}
/**
* Gets the value of the suppressDropsForever property.
*
* @return
* possible object is
* {@link Boolean }
*
*/
public Boolean isSuppressDropsForever() {
return suppressDropsForever;
}
/**
* Sets the value of the suppressDropsForever property.
*
* @param value
* allowed object is
* {@link Boolean }
*
*/
public void setSuppressDropsForever(Boolean value) {
this.suppressDropsForever = value;
}
/**
* Gets the value of the generated property.
*
@@ -15,7 +15,7 @@ import javax.xml.bind.annotation.XmlType;
* &lt;simpleType name="changeSetType">
* &lt;restriction base="{http://www.w3.org/2001/XMLSchema}string">
* &lt;enumeration value="apply"/>
* &lt;enumeration value="drop"/>
* &lt;enumeration value="pendingDrops"/>
* &lt;enumeration value="baseline"/>
* &lt;/restriction>
* &lt;/simpleType>
@@ -28,8 +28,8 @@ public enum ChangeSetType {
@XmlEnumValue("apply")
APPLY("apply"),
@XmlEnumValue("drop")
DROP("drop"),
@XmlEnumValue("pendingDrops")
PENDING_DROPS("pendingDrops"),
@XmlEnumValue("baseline")
BASELINE("baseline");
private final String value;
@@ -30,6 +30,7 @@ import javax.xml.bind.annotation.XmlType;
* &lt;attGroup ref="{http://ebean-orm.github.io/xml/ns/dbmigration}tablespaceAttributes"/>
* &lt;attribute name="name" use="required" type="{http://www.w3.org/2001/XMLSchema}string" />
* &lt;attribute name="withHistory" type="{http://www.w3.org/2001/XMLSchema}boolean" />
* &lt;attribute name="draft" type="{http://www.w3.org/2001/XMLSchema}boolean" />
* &lt;attribute name="identityType" type="{http://ebean-orm.github.io/xml/ns/dbmigration}identityType" />
* &lt;attribute name="sequenceName" type="{http://www.w3.org/2001/XMLSchema}string" />
* &lt;attribute name="sequenceInitial" type="{http://www.w3.org/2001/XMLSchema}positiveInteger" />
@@ -59,6 +60,8 @@ public class CreateTable {
protected String name;
@XmlAttribute(name = "withHistory")
protected Boolean withHistory;
@XmlAttribute(name = "draft")
protected Boolean draft;
@XmlAttribute(name = "identityType")
protected IdentityType identityType;
@XmlAttribute(name = "sequenceName")
@@ -213,6 +216,30 @@ public class CreateTable {
this.withHistory = value;
}
/**
* Gets the value of the draft property.
*
* @return
* possible object is
* {@link Boolean }
*
*/
public Boolean isDraft() {
return draft;
}
/**
* Sets the value of the draft property.
*
* @param value
* allowed object is
* {@link Boolean }
*
*/
public void setDraft(Boolean value) {
this.draft = value;
}
/**
* Gets the value of the identityType property.
*
@@ -7,22 +7,46 @@ import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
/**
* Simple writer for output of the Migration/ChangeSet as an XML document.
*/
public class MigrationXmlWriter {
private final String comment;
public MigrationXmlWriter(String comment) {
this.comment = comment;
}
/**
* Write a Migration to a file as an xml document to the file.
*/
public void write(Migration migration, File file) {
try {
FileWriter writer = new FileWriter(file);
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
if (comment != null) {
writer.write("<!-- ");
writer.write(comment);
writer.write(" -->\n");
}
JAXBContext jaxbContext = JAXBContext.newInstance(Migration.class);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(migration, file);
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
marshaller.marshal(migration, writer);
writer.close();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (JAXBException e) {
throw new RuntimeException(e);
@@ -117,13 +117,13 @@ public class CurrentModel {
/**
* Return the 'Drop' DDL.
*/
public String getDropDdl() throws IOException {
public String getDropAllDdl() throws IOException {
createDdl();
StringBuilder ddl = new StringBuilder(2000);
ddl.append(write.rollbackForeignKeys().getBuffer());
ddl.append(write.rollback().getBuffer());
ddl.append(write.dropAllForeignKeys().getBuffer());
ddl.append(write.dropAll().getBuffer());
return ddl.toString();
}
@@ -159,12 +159,7 @@ public class CurrentModel {
ModelDiff diff = new ModelDiff();
diff.compareTo(model);
List<Object> applyChanges = diff.getApplyChanges();
// put the changes into a ChangeSet
ChangeSet applyChangeSet = new ChangeSet();
applyChangeSet.getChangeSetChildren().addAll(applyChanges);
return applyChangeSet;
return diff.getApplyChangeSet();
}
}
@@ -283,7 +283,6 @@ public class MColumn {
/**
* Compare the column meta data and return true if there is a change that means
* the history table column needs
*/
public void compare(ModelDiff modelDiff, MTable table, MColumn newColumn) {
@@ -10,13 +10,14 @@ import com.avaje.ebean.dbmigration.migration.DropHistoryTable;
import com.avaje.ebean.dbmigration.migration.DropTable;
import com.avaje.ebean.dbmigration.migration.IdentityType;
import com.avaje.ebean.dbmigration.migration.UniqueConstraint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -38,6 +39,8 @@ import java.util.Set;
*/
public class MTable {
private static final Logger logger = LoggerFactory.getLogger(MTable.class);
/**
* Table name.
*/
@@ -118,6 +121,8 @@ public class MTable {
*/
private AddColumn addColumn;
private List<String> droppedColumns = new ArrayList<String>();
/**
* Create a copy of this table structure as a 'draft' table.
*
@@ -126,14 +131,14 @@ public class MTable {
*/
public MTable createDraftTable() {
draftTable = new MTable(name+"_draft");
draftTable = new MTable(name + "_draft");
draftTable.draft = true;
draftTable.whenCreatedColumn = whenCreatedColumn;
// compoundKeys
// compoundUniqueConstraints
draftTable.identityType = identityType;
for (MColumn col: columns.values()) {
for (MColumn col : allColumns()) {
draftTable.addColumn(col.copyForDraft());
}
@@ -150,6 +155,7 @@ public class MTable {
this.tablespace = createTable.getTablespace();
this.indexTablespace = createTable.getIndexTablespace();
this.withHistory = Boolean.TRUE.equals(createTable.isWithHistory());
this.draft = Boolean.TRUE.equals(createTable.isDraft());
this.sequenceName = createTable.getSequenceName();
this.sequenceInitial = toInt(createTable.getSequenceInitial());
this.sequenceAllocate = toInt(createTable.getSequenceAllocate());
@@ -194,8 +200,11 @@ public class MTable {
if (withHistory) {
createTable.setWithHistory(Boolean.TRUE);
}
if (draft) {
createTable.setDraft(Boolean.TRUE);
}
for (MColumn column : this.columns.values()) {
for (MColumn column : allColumns()) {
// filter out draftOnly columns from the base table
if (draft || !column.isDraftOnly()) {
createTable.getColumn().add(column.createColumn());
@@ -244,26 +253,33 @@ public class MTable {
addColumn = null;
Set<String> mappedColumns = new LinkedHashSet<String>();
Map<String, MColumn> newColumnMap = newTable.getColumns();
Collection<MColumn> newColumns = newTable.getColumns().values();
for (MColumn newColumn : newColumns) {
MColumn localColumn = columns.get(newColumn.getName());
// compare newColumns to existing columns (look for new and diff columns)
for (MColumn newColumn : newColumnMap.values()) {
MColumn localColumn = getColumn(newColumn.getName());
if (localColumn == null) {
diffNewColumn(newColumn);
// can ignore if draftOnly column and non-draft table
if (!newColumn.isDraftOnly() || draft) {
diffNewColumn(newColumn);
}
} else {
// note that if there are alter column changes in here then
// the table withHistory is taken into account
localColumn.compare(modelDiff, this, newColumn);
mappedColumns.add(newColumn.getName());
}
}
Collection<MColumn> existingColumns = columns.values();
for (MColumn existingColumn : existingColumns) {
if (!mappedColumns.contains(existingColumn.getName())) {
diffDropColumn(modelDiff, existingColumn);
// compare existing columns (look for dropped columns)
int columnPosition = 0;
for (MColumn existingColumn : allColumns()) {
MColumn newColumn = newColumnMap.get(existingColumn.getName());
if (newColumn == null) {
diffDropColumn(modelDiff, existingColumn, columnPosition, newTable);
} else if (newColumn.isDraftOnly() && !draft) {
// effectively a drop column (draft only column on a non-draft table)
logger.trace("... drop column {} from table {} as now draftOnly", newColumn.getName(), name);
diffDropColumn(modelDiff, existingColumn, columnPosition, newTable);
}
columnPosition++;
}
if (addColumn != null) {
@@ -287,7 +303,7 @@ public class MTable {
public void apply(AlterColumn alterColumn) {
checkTableName(alterColumn.getTableName());
String columnName = alterColumn.getColumnName();
MColumn existingColumn = columns.get(columnName);
MColumn existingColumn = getColumn(columnName);
if (existingColumn == null) {
throw new IllegalStateException("Column [" + columnName + "] does not exist for AlterColumn change?");
}
@@ -299,7 +315,10 @@ public class MTable {
*/
public void apply(DropColumn dropColumn) {
checkTableName(dropColumn.getTableName());
columns.remove(dropColumn.getColumnName());
MColumn removed = columns.remove(dropColumn.getColumnName());
if (removed == null) {
throw new IllegalStateException("Column [" + dropColumn.getColumnName() + "] does not exist for DropColumn change on table [" + dropColumn.getTableName() + "]?");
}
}
public String getName() {
@@ -313,10 +332,6 @@ public class MTable {
return draft;
}
public String getPkName() {
return pkName;
}
public void setPkName(String pkName) {
this.pkName = pkName;
}
@@ -341,11 +356,43 @@ public class MTable {
return withHistory;
}
public void setWithHistory(boolean withHistory) {
public MTable setWithHistory(boolean withHistory) {
this.withHistory = withHistory;
return this;
}
public Map<String, MColumn> getColumns() {
public List<String> allHistoryColumns(boolean includeDropped) {
List<String> columnNames = new ArrayList<String>(columns.size());
for (MColumn column : columns.values()) {
if (column.isIncludeInHistory()) {
columnNames.add(column.getName());
}
}
if (includeDropped && !droppedColumns.isEmpty()) {
for (String droppedColumn : droppedColumns) {
columnNames.add(droppedColumn);
}
}
return columnNames;
}
/**
* Return all the columns (excluding columns marked as dropped).
*/
public Collection<MColumn> allColumns() {
return columns.values();
}
/**
* Return the column by name.
*/
public MColumn getColumn(String name) {
return columns.get(name);
}
private Map<String, MColumn> getColumns() {
return columns;
}
@@ -357,26 +404,14 @@ public class MTable {
return compoundKeys;
}
public String getSequenceName() {
return sequenceName;
}
public void setSequenceName(String sequenceName) {
this.sequenceName = sequenceName;
}
public int getSequenceInitial() {
return sequenceInitial;
}
public void setSequenceInitial(int sequenceInitial) {
this.sequenceInitial = sequenceInitial;
}
public int getSequenceAllocate() {
return sequenceAllocate;
}
public void setSequenceAllocate(int sequenceAllocate) {
this.sequenceAllocate = sequenceAllocate;
}
@@ -399,22 +434,12 @@ public class MTable {
this.identityType = identityType;
}
/**
* Returns the identity type to use for this table.
* <p>
* If set then this overrides the platform default so for UUID generated values
* or DB's supporting both sequences and autoincrement.
*/
public IdentityType getIdentityType() {
return identityType;
}
/**
* Return the list of columns that make the primary key.
*/
public List<MColumn> primaryKeyColumns() {
List<MColumn> pk = new ArrayList<MColumn>(3);
for (MColumn column : columns.values()) {
for (MColumn column : allColumns()) {
if (column.isPrimaryKey()) {
pk.add(column);
}
@@ -473,7 +498,7 @@ public class MTable {
*/
public MColumn addColumn(String dbCol, String columnDefn, boolean notnull) {
MColumn existingColumn = columns.get(dbCol);
MColumn existingColumn = getColumn(dbCol);
if (existingColumn != null) {
if (notnull) {
existingColumn.setNotnull(true);
@@ -507,12 +532,12 @@ public class MTable {
/**
* Add a 'drop column' to the diff.
*/
private void diffDropColumn(ModelDiff modelDiff, MColumn existingColumn) {
private void diffDropColumn(ModelDiff modelDiff, MColumn existingColumn, int columnPosition, MTable newTable) {
DropColumn dropColumn = new DropColumn();
dropColumn.setTableName(name);
dropColumn.setColumnName(existingColumn.getName());
if (withHistory) {
if (withHistory && !existingColumn.isHistoryExclude()) {
// These dropColumns should occur on the history
// table as well as the base table
dropColumn.setWithHistory(Boolean.TRUE);
@@ -521,6 +546,16 @@ public class MTable {
modelDiff.addDropColumn(dropColumn);
}
/**
* Register a pending un-applied drop column.
* <p>
* This means this column still needs to be included in history views/triggers etc even
* though it is not part of the current model.
*/
public void registerPendingDropColumn(String columnName) {
droppedColumns.add(columnName);
}
private int toInt(BigInteger value) {
return (value == null) ? 0 : value.intValue();
}
@@ -532,7 +567,7 @@ public class MTable {
/**
* Check if there are duplicate foreign keys.
* <p>
* This can occur when an ManyToMany relates back to itself.
* This can occur when an ManyToMany relates back to itself.
* </p>
*/
public void checkDuplicateForeignKeys() {
@@ -563,7 +598,7 @@ public class MTable {
*/
public void adjustReferences(ModelContainer modelContainer) {
Collection<MColumn> cols = columns.values();
Collection<MColumn> cols = allColumns();
for (MColumn col : cols) {
String references = col.getReferences();
if (references != null) {
@@ -583,7 +618,7 @@ public class MTable {
*/
private String extractBaseTable(String references) {
int lastDot = references.lastIndexOf('.');
return references.substring(0,lastDot);
return references.substring(0, lastDot);
}
/**
@@ -592,7 +627,7 @@ public class MTable {
*/
private String deriveReferences(String references, String draftTableName) {
int lastDot = references.lastIndexOf('.');
return draftTableName+"."+references.substring(lastDot+1);
return draftTableName + "." + references.substring(lastDot + 1);
}
}
@@ -52,7 +52,7 @@ public class MigrationModel {
List<MigrationResource> resources = new ArrayList<MigrationResource>();
for (File xmlFile: xmlFiles) {
resources.add(new MigrationResource(xmlFile));
resources.add(new MigrationResource(xmlFile, createVersion(xmlFile)));
}
// sort into version order before applying
@@ -60,7 +60,7 @@ public class MigrationModel {
for (MigrationResource migrationResource: resources) {
logger.debug("read {}", migrationResource);
model.apply(migrationResource.read());
model.apply(migrationResource.read(), migrationResource.getVersion());
}
// remember the last version
@@ -69,6 +69,12 @@ public class MigrationModel {
}
}
private MigrationVersion createVersion(File xmlFile) {
String fileName = xmlFile.getName();
String versionName = fileName.substring(0, fileName.length() - modelSuffix.length());
return MigrationVersion.parse(versionName);
}
public String getNextVersion(String initialVersion) {
return lastVersion == null ? initialVersion : lastVersion.nextVersion();
@@ -17,9 +17,9 @@ public class MigrationResource implements Comparable<MigrationResource> {
/**
* Construct with a migration xml file.
*/
public MigrationResource(File migrationFile) {
public MigrationResource(File migrationFile, MigrationVersion version) {
this.migrationFile = migrationFile;
this.version = MigrationVersion.parse(migrationFile.getName());
this.version = version;
}
public String toString() {
@@ -1,5 +1,7 @@
package com.avaje.ebean.dbmigration.model;
import java.util.Arrays;
/**
* The version of a migration used so that migrations are processed in order.
*/
@@ -15,23 +17,74 @@ public class MigrationVersion implements Comparable<MigrationVersion> {
*/
private final int[] ordering;
private MigrationVersion(String raw, int[] ordering) {
private final boolean[] underscores;
private final String comment;
private MigrationVersion(String raw, int[] ordering, boolean[] underscores, String comment) {
this.raw = raw;
this.ordering = ordering;
this.underscores = underscores;
this.comment = comment;
}
public String toString() {
return raw;
}
/**
* Return the version comment.
*/
public String getComment() {
return comment;
}
/**
* Return the version in raw form.
*/
public String getRaw() {
return raw;
}
/**
* Return the trimmed version excluding version comment and un-parsable string.
*/
public String asString() {
return formattedVersion(false, false);
}
/**
* Return the trimmed version with any underscores replaced with '.'
*/
public String normalised() {
return formattedVersion(true, false);
}
/**
* Return the next version based on this version.
*/
public String nextVersion() {
return formattedVersion(false, true);
}
/**
* Returns the version part of the string.
*
* Normalised means always use '.' delimiters (no underscores).
* NextVersion means bump/increase the last version number by 1.
*/
private String formattedVersion(boolean normalised, boolean nextVersion) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ordering.length; i++) {
if (i < ordering.length -1 ) {
sb.append(ordering[i]).append(".");
if (i < ordering.length - 1) {
sb.append(ordering[i]);
if (normalised) {
sb.append('.');
} else {
sb.append(underscores[i] ? '_' : '.');
}
} else {
sb.append(ordering[i]+1);
sb.append((nextVersion) ? ordering[i] + 1 : ordering[i]);
}
}
return sb.toString();
@@ -54,33 +107,54 @@ public class MigrationVersion implements Comparable<MigrationVersion> {
return 0;
}
/**
* Parse the raw version string and just return the leading version number;
*/
public static String trim(String raw) {
return parse(raw).asString();
}
/**
* Parse the raw version string into a MigrationVersion.
*/
public static MigrationVersion parse(String raw) {
String value = raw.replace("__",".");
value = value.replace('_','.');
String comment = "";
String value = raw;
int commentStart = raw.indexOf("__");
if (commentStart > -1) {
// trim off the trailing comment
comment = raw.substring(commentStart + 2);
value = value.substring(0, commentStart);
}
value = value.replace('_', '.');
String[] sections = value.split("\\.");
boolean[] underscores = new boolean[sections.length];
int[] ordering = new int[sections.length];
int delimiterPos = 0;
int stopIndex = 0;
for (int i = 0; i < sections.length; i++) {
try {
ordering[i] = Integer.parseInt(sections[i]);
stopIndex++;
delimiterPos += sections[i].length();
underscores[i] = (delimiterPos < raw.length() - 1 && raw.charAt(delimiterPos) == '_');
delimiterPos++;
} catch (NumberFormatException e) {
// stop parsing
break;
}
}
int[] actualOrder = new int[stopIndex];
System.arraycopy(ordering, 0, actualOrder, 0, stopIndex);
int[] actualOrder = Arrays.copyOf(ordering, stopIndex);
boolean[] actualUnderscores = Arrays.copyOf(underscores, stopIndex);
return new MigrationVersion(raw, actualOrder);
return new MigrationVersion(raw, actualOrder, actualUnderscores, comment);
}
}
@@ -4,6 +4,7 @@ import com.avaje.ebean.dbmigration.migration.AddColumn;
import com.avaje.ebean.dbmigration.migration.AddHistoryTable;
import com.avaje.ebean.dbmigration.migration.AlterColumn;
import com.avaje.ebean.dbmigration.migration.ChangeSet;
import com.avaje.ebean.dbmigration.migration.ChangeSetType;
import com.avaje.ebean.dbmigration.migration.CreateIndex;
import com.avaje.ebean.dbmigration.migration.CreateTable;
import com.avaje.ebean.dbmigration.migration.DropColumn;
@@ -12,7 +13,6 @@ import com.avaje.ebean.dbmigration.migration.DropIndex;
import com.avaje.ebean.dbmigration.migration.DropTable;
import com.avaje.ebean.dbmigration.migration.Migration;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -28,23 +28,23 @@ public class ModelContainer {
/**
* All the tables in the model.
*/
private Map<String, MTable> tables = new LinkedHashMap<String, MTable>();
private final Map<String, MTable> tables = new LinkedHashMap<String, MTable>();
/**
* All the non unique non foreign key indexes.
*/
private Map<String, MIndex> indexes = new LinkedHashMap<String, MIndex>();
private final Map<String, MIndex> indexes = new LinkedHashMap<String, MIndex>();
private final PendingDrops pendingDrops = new PendingDrops();
public ModelContainer() {
}
/**
* Adjust the FK references on all the draft tables.
*/
public void adjustDraftReferences() {
Collection<MTable> tables = this.tables.values();
for (MTable table : tables) {
for (MTable table : this.tables.values()) {
if (table.isDraft()) {
table.adjustReferences(this);
}
@@ -82,14 +82,31 @@ public class ModelContainer {
/**
* Apply a migration with associated changeSets to the model.
*/
public void apply(Migration migration) {
public void apply(Migration migration, MigrationVersion version) {
List<ChangeSet> changeSets = migration.getChangeSet();
for (ChangeSet changeSet : changeSets) {
applyChangeSet(changeSet);
boolean pending = changeSet.getType() == ChangeSetType.PENDING_DROPS;
if (pending) {
// un-applied drop columns etc
pendingDrops.add(version, changeSet);
} else if (isDropsFor(changeSet)) {
pendingDrops.appliedDropsFor(changeSet);
}
if (!isDropsFor(changeSet)) {
applyChangeSet(changeSet);
}
}
}
/**
* Return true if the changeSet contains drops for a previous PENDING_DROPS changeSet.
*/
private boolean isDropsFor(ChangeSet changeSet) {
return changeSet.getDropsFor() != null;
}
/**
* Apply a changeSet to the model.
*/
@@ -245,4 +262,53 @@ public class ModelContainer {
indexes.put(indexName, new MIndex(indexName, tableName, columnNames));
}
/**
* Return the list of versions containing un-applied pending drops.
*/
public List<String> getPendingDrops() {
return pendingDrops.pendingDrops();
}
/**
* Return the migration for the pending drops for a given version.
*/
public Migration migrationForPendingDrop(String pendingVersion) {
return pendingDrops.migrationForVersion(pendingVersion);
}
/**
* Register the drop columns on history tables that have not been applied yet.
*/
public void registerPendingHistoryDropColumns(ModelContainer newModel) {
pendingDrops.registerPendingHistoryDropColumns(newModel);
}
/**
* Register any pending drop columns on history tables. These columns are now not in the current
* logical model but we still need to include them in the history views and triggers until they
* are actually dropped.
*/
public void registerPendingHistoryDropColumns(ChangeSet changeSet) {
for (Object change : changeSet.getChangeSetChildren()) {
if (change instanceof DropColumn) {
DropColumn dropColumn = (DropColumn) change;
if (Boolean.TRUE.equals(dropColumn.isWithHistory())) {
registerPendingDropColumn(dropColumn);
}
}
}
}
/**
* Register a drop column on a history tables that has not been applied yet.
*/
private void registerPendingDropColumn(DropColumn dropColumn) {
MTable table = getTable(dropColumn.getTableName());
if (table == null) {
throw new IllegalArgumentException("Table ["+dropColumn.getTableName()+"] not found?");
}
table.registerPendingDropColumn(dropColumn.getColumnName());
}
}
@@ -66,16 +66,14 @@ public class ModelDiff {
public Migration getMigration() {
Migration migration = new Migration();
ChangeSet applyChangeSet = getApplyChangeSet();
if (!applyChangeSet.getChangeSetChildren().isEmpty()) {
if (!applyChanges.isEmpty()) {
// add a non empty apply changeSet
migration.getChangeSet().add(applyChangeSet);
migration.getChangeSet().add(getApplyChangeSet());
}
ChangeSet dropChangeSet = getDropChangeSet();
if (!dropChangeSet.getChangeSetChildren().isEmpty()) {
if (!dropChanges.isEmpty()) {
// add a non empty drop changeSet
migration.getChangeSet().add(dropChangeSet);
migration.getChangeSet().add(getDropChangeSet());
}
return migration;
}
@@ -83,14 +81,14 @@ public class ModelDiff {
/**
* Return the list of 'apply' changes.
*/
public List<Object> getApplyChanges() {
List<Object> getApplyChanges() {
return applyChanges;
}
/**
* Return the list of 'drop' changes.
*/
public List<Object> getDropChanges() {
List<Object> getDropChanges() {
return dropChanges;
}
@@ -108,10 +106,10 @@ public class ModelDiff {
/**
* Return the 'drop' changeSet.
*/
public ChangeSet getDropChangeSet() {
ChangeSet getDropChangeSet() {
// put the changes into a ChangeSet
ChangeSet createChangeSet = new ChangeSet();
createChangeSet.setType(ChangeSetType.DROP);
createChangeSet.setType(ChangeSetType.PENDING_DROPS);
createChangeSet.getChangeSetChildren().addAll(dropChanges);
return createChangeSet;
}
@@ -156,6 +154,12 @@ public class ModelDiff {
}
}
// register un-applied ones from the previous migrations
baseModel.registerPendingHistoryDropColumns(newModel);
if (!dropChanges.isEmpty()) {
// register new ones created just now as part of this diff
newModel.registerPendingHistoryDropColumns(getDropChangeSet());
}
}
protected void addDropTable(MTable existingTable) {
@@ -0,0 +1,263 @@
package com.avaje.ebean.dbmigration.model;
import com.avaje.ebean.dbmigration.migration.ChangeSet;
import com.avaje.ebean.dbmigration.migration.ChangeSetType;
import com.avaje.ebean.dbmigration.migration.DropColumn;
import com.avaje.ebean.dbmigration.migration.DropTable;
import com.avaje.ebean.dbmigration.migration.Migration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
/**
* The migrations with pending un-applied drops.
*/
public class PendingDrops {
private final LinkedHashMap<String, Entry> map = new LinkedHashMap<String, Entry>();
/**
* Add a 'pending drops' changeSet for the given version.
*/
public void add(MigrationVersion version, ChangeSet changeSet) {
Entry entry = map.get(version.normalised());
if (entry == null) {
entry = new Entry(version);
map.put(version.normalised(), entry);
}
entry.add(changeSet);
}
/**
* Return the list of versions with pending drops.
*/
public List<String> pendingDrops() {
List<String> versions = new ArrayList<String>();
for (Entry value : map.values()) {
if (value.hasPendingDrops()) {
versions.add(value.version.asString());
}
}
return versions;
}
/**
* All the pending drops for this migration version have been applied so we need
* to remove the (unsuppressed) pending drops for this version.
*/
public boolean appliedDropsFor(ChangeSet changeSet) {
MigrationVersion version = MigrationVersion.parse(changeSet.getDropsFor());
Entry entry = map.get(version.normalised());
if (entry.removeDrops(changeSet)) {
// it had no suppressForever changeSets so remove completely
map.remove(version.normalised());
return true;
}
return false;
}
/**
* Return the migration for the pending drops from a version.
* <p>
* The value of version can be "next" to find the first un-applied pending drops.
* </p>
*/
public Migration migrationForVersion(String pendingVersion) {
Entry entry = getEntry(pendingVersion);
Migration migration = new Migration();
Iterator<ChangeSet> it = entry.list.iterator();
while (it.hasNext()) {
ChangeSet changeSet = it.next();
if (!isSuppressForever(changeSet)) {
it.remove();
changeSet.setType(ChangeSetType.APPLY);
changeSet.setDropsFor(entry.version.asString());
migration.getChangeSet().add(changeSet);
}
}
if (migration.getChangeSet().isEmpty()) {
throw new IllegalArgumentException("The remaining pendingDrops changeSets in migration ["+pendingVersion+"] are suppressDropsForever=true and can't be applied");
}
if (!entry.containsSuppressForever()) {
// we can remove it completely as it has no suppressForever changes
map.remove(entry.version.normalised());
}
return migration;
}
private Entry getEntry(String pendingVersion) {
if ("next".equalsIgnoreCase(pendingVersion)) {
Iterator<Entry> it = map.values().iterator();
if (it.hasNext()) {
return it.next();
}
} else {
Entry remove = map.get(MigrationVersion.parse(pendingVersion).normalised());
if (remove != null) {
return remove;
}
}
throw new IllegalArgumentException("No 'pendingDrops' changeSets for migration version [" + pendingVersion + "] found");
}
/**
* Register pending drop columns on history tables to the new model.
*/
public void registerPendingHistoryDropColumns(ModelContainer newModel) {
for (Entry entry : map.values()) {
for (ChangeSet changeSet : entry.list) {
newModel.registerPendingHistoryDropColumns(changeSet);
}
}
}
/**
* Return true if there is an Entry for the given version.
*/
boolean testContainsEntryFor(MigrationVersion version) {
return map.containsKey(version.normalised());
}
/**
* Return the Entry for the given version.
*/
Entry testGetEntryFor(MigrationVersion version) {
return map.get(version.normalised());
}
static class Entry {
final MigrationVersion version;
final List<ChangeSet> list = new ArrayList<ChangeSet>();
Entry(MigrationVersion version) {
this.version = version;
}
void add(ChangeSet changeSet) {
list.add(changeSet);
}
/**
* Return true if this contains suppressForever changeSets.
*/
boolean containsSuppressForever() {
for (ChangeSet changeSet : list) {
if (isSuppressForever(changeSet)) {
return true;
}
}
return false;
}
/**
* Return true if this contains drops that can be applied / migrated.
*/
boolean hasPendingDrops() {
for (ChangeSet changeSet : list) {
if (!isSuppressForever(changeSet)) {
return true;
}
}
return false;
}
/**
* Remove the drops that are not suppressForever and return true if that
* removed all the changeSets (and there are no suppressForever ones).
*/
boolean removeDrops(ChangeSet appliedDrops) {
Iterator<ChangeSet> iterator = list.iterator();
while (iterator.hasNext()) {
ChangeSet next = iterator.next();
if (!isSuppressForever(next)) {
removeMatchingChanges(next, appliedDrops);
if (next.getChangeSetChildren().isEmpty()) {
iterator.remove();
}
}
}
return list.isEmpty();
}
/**
* Remove the applied drops from the pending ones matching by table name and column name.
*/
private void removeMatchingChanges(ChangeSet pendingDrops, ChangeSet appliedDrops) {
List<Object> pending = pendingDrops.getChangeSetChildren();
Iterator<Object> iterator = pending.iterator();
while (iterator.hasNext()) {
Object pendingDrop = iterator.next();
if (pendingDrop instanceof DropColumn && dropColumnIn((DropColumn)pendingDrop, appliedDrops)) {
iterator.remove();
} else if (pendingDrop instanceof DropTable && dropTableIn((DropTable)pendingDrop, appliedDrops)) {
iterator.remove();
}
}
}
/**
* Return true if the pendingDrop is contained in the appliedDrops.
*/
private boolean dropTableIn(DropTable pendingDrop, ChangeSet appliedDrops) {
for (Object o : appliedDrops.getChangeSetChildren()) {
if (o instanceof DropTable && sameTable(pendingDrop, (DropTable)o)) {
return true;
}
}
return false;
}
/**
* Return true if the pendingDrop is contained in the appliedDrops.
*/
private boolean dropColumnIn(DropColumn pendingDrop, ChangeSet appliedDrops) {
for (Object o : appliedDrops.getChangeSetChildren()) {
if (o instanceof DropColumn && sameColumn(pendingDrop, (DropColumn) o)) {
return true;
}
}
return false;
}
/**
* Return true if the DropTable match by table name.
*/
private boolean sameTable(DropTable pendingDrop, DropTable o) {
return pendingDrop.getName().equals(o.getName());
}
/**
* Return true if the DropColumns match by table and column name.
*/
private boolean sameColumn(DropColumn pending, DropColumn o) {
return pending.getColumnName().equals(o.getColumnName())
&& pending.getTableName().equals(o.getTableName());
}
}
private static boolean isSuppressForever(ChangeSet next) {
return Boolean.TRUE.equals(next.isSuppressDropsForever());
}
}
@@ -3,9 +3,11 @@ package com.avaje.ebean.dbmigration.model;
import com.avaje.ebean.config.DbMigrationConfig;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlHandler;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
import com.avaje.ebean.dbmigration.migration.ChangeSet;
import com.avaje.ebean.dbmigration.migration.ChangeSetType;
import com.avaje.ebean.dbmigration.migration.Migration;
import java.io.File;
@@ -43,7 +45,7 @@ public class PlatformDdlWriter {
List<ChangeSet> changeSets = dbMigration.getChangeSet();
for (ChangeSet changeSet : changeSets) {
if (!changeSet.getChangeSetChildren().isEmpty()) {
if (isApply(changeSet)) {
handler.generate(write, changeSet);
}
}
@@ -52,6 +54,13 @@ public class PlatformDdlWriter {
writePlatformDdl(write, writePath, fullVersion);
}
/**
* Return true if the changeSet is APPLY and not empty.
*/
private boolean isApply(ChangeSet changeSet) {
return changeSet.getType() == ChangeSetType.APPLY && !changeSet.getChangeSetChildren().isEmpty();
}
/**
* Write the ddl files.
*/
@@ -65,26 +74,6 @@ public class PlatformDdlWriter {
} finally {
applyWriter.close();
}
if (!config.isSuppressRollback() && !write.isApplyRollbackEmpty()) {
FileWriter applyRollbackWriter = createWriter(resourcePath, fullVersion, config.getRollbackPath(), config.getRollbackSuffix());
try {
writeApplyRollbackDdl(applyRollbackWriter, write);
applyRollbackWriter.flush();
} finally {
applyRollbackWriter.close();
}
}
}
if (!write.isDropEmpty()) {
FileWriter dropWriter = createWriter(resourcePath, fullVersion, config.getDropPath(), config.getDropSuffix());
try {
writeDropDdl(dropWriter, write);
dropWriter.flush();
} finally {
dropWriter.close();
}
}
}
@@ -116,29 +105,19 @@ public class PlatformDdlWriter {
protected void writeApplyDdl(Writer writer, DdlWrite write) throws IOException {
// merge the apply buffers in the appropriate order
prependDropDependencies(writer, write.applyDropDependencies());
writer.append("-- apply changes\n");
writer.append(write.apply().getBuffer());
writer.append(write.applyForeignKeys().getBuffer());
writer.append(write.applyHistory().getBuffer());
}
/**
* Write the 'Rollback' DDL buffers to the writer.
*/
protected void writeApplyRollbackDdl(Writer writer, DdlWrite write) throws IOException {
// merge the rollback buffers in the appropriate order
writer.append(write.rollbackForeignKeys().getBuffer());
writer.append(write.rollback().getBuffer());
}
/**
* Write the 'Drop' DDL buffers to the writer.
*/
protected void writeDropDdl(Writer writer, DdlWrite write) throws IOException {
// merge the rollback buffers in the appropriate order
writer.append(write.dropHistory().getBuffer());
writer.append(write.drop().getBuffer());
private void prependDropDependencies(Writer writer, DdlBuffer buffer) throws IOException {
if (!buffer.isEmpty()) {
writer.append("-- drop dependencies\n");
writer.append(buffer.getBuffer());
writer.append("\n");
}
}
/**
@@ -38,7 +38,7 @@ public class ModelBuildBeanVisitor implements BeanVisitor {
table.setComment(descriptor.getDbComment());
if (descriptor.isHistorySupport()) {
table.setWithHistory(true);
BeanProperty whenCreated = descriptor.findWhenCreatedProperty();
BeanProperty whenCreated = descriptor.getWhenCreatedProperty();
if (whenCreated != null) {
table.setWhenCreatedColumn(whenCreated.getDbColumn());
}
@@ -158,7 +158,7 @@ public class ModelBuildContext {
int fkCount = 0;
int ixCount = 0;
int uqCount = 0;
Collection<MColumn> cols = draftTable.getColumns().values();
Collection<MColumn> cols = draftTable.allColumns();
for (MColumn col: cols) {
if (col.getForeignKeyName() != null) {
// Note that we adjust the 'references' table later in a second pass
@@ -84,7 +84,7 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
table.setPkName(determinePrimaryKeyName());
// check if indexes on foreign keys should be suppressed
for (MColumn column : table.getColumns().values()) {
for (MColumn column : table.allColumns()) {
if (hasValue(column.getForeignKeyIndex())) {
if (indexSet.contains(column.getName())) {
// suppress index on foreign key as there is already
@@ -0,0 +1,72 @@
package com.avaje.ebean.plugin;
import com.avaje.ebean.FetchPath;
import com.avaje.ebean.Query;
import com.avaje.ebeanservice.docstore.api.DocStoreUpdateContext;
import java.io.IOException;
/**
* Doc store functions for a specific entity bean type.
*
* @param <T> The type of entity bean
*/
public interface BeanDocType<T> {
/**
* Return the doc store index type for this bean type.
*/
String getIndexType();
/**
* Return the doc store index name for this bean type.
*/
String getIndexName();
/**
* Apply the appropriate fetch path to the query such that the query returns beans matching
* the document store structure with the expected embedded properties.
*/
void applyPath(Query<T> spiQuery);
/**
* Return the FetchPath for the embedded document.
*/
FetchPath getEmbedded(String path);
/**
* For embedded 'many' properties we need a FetchPath relative to the root which is used to
* build and replace the embedded list.
*/
FetchPath getEmbeddedManyRoot(String path);
/**
* Return a 'raw' property mapped for the given property.
* If none exists the given property is returned.
*/
String rawProperty(String property);
/**
* Store the bean in the doc store index.
* <p>
* This somewhat assumes the bean is fetched with appropriate path properties
* to match the expected document structure.
*/
void index(Object idValue, T bean, DocStoreUpdateContext txn) throws IOException;
/**
* Add a delete by Id to the doc store.
*/
void deleteById(Object idValue, DocStoreUpdateContext txn) throws IOException;
/**
* Add a embedded document update to the doc store.
*
* @param idValue the Id value of the bean holding the embedded document
* @param embeddedProperty the embedded property
* @param embeddedRawContent the content of the embedded document in JSON form
* @param txn the doc store transaction to add the update to
*/
void updateEmbedded(Object idValue, String embeddedProperty, String embeddedRawContent, DocStoreUpdateContext txn) throws IOException;
}
@@ -0,0 +1,179 @@
package com.avaje.ebean.plugin;
import com.avaje.ebean.config.dbplatform.IdType;
import com.avaje.ebean.event.BeanFindController;
import com.avaje.ebean.event.BeanPersistController;
import com.avaje.ebean.event.BeanPersistListener;
import com.avaje.ebean.event.BeanQueryAdapter;
import com.avaje.ebeaninternal.api.SpiQuery;
import com.avaje.ebeanservice.docstore.api.mapping.DocumentMapping;
import java.util.Collection;
/**
* Information and methods on BeanDescriptors made available to plugins.
*/
public interface BeanType<T> {
/**
* Return the short name of the bean type.
*/
String getName();
/**
* Return the full name of the bean type.
*/
String getFullName();
/**
* Return the class type this BeanDescriptor describes.
*/
Class<T> getBeanType();
/**
* Return the type bean for an OneToMany or ManyToOne or ManyToMany property.
*/
BeanType<?> getBeanTypeAtPath(String propertyName);
/**
* Return all the properties for this bean type.
*/
Collection<? extends Property> allProperties();
/**
* Return the Id property.
*/
Property getIdProperty();
/**
* Return the when modified property if there is one defined.
*/
Property getWhenModifiedProperty();
/**
* Return the when created property if there is one defined.
*/
Property getWhenCreatedProperty();
/**
* Return the Property to read values from a bean.
*/
Property getProperty(String propertyName);
/**
* Return the ExpressionPath for a given property path.
* <p>
* This can return a property or nested property path.
* </p>
*/
ExpressionPath getExpressionPath(String path);
/**
* Return true if the property is a valid known property or path for the given bean type.
*/
boolean isValidExpression(String property);
/**
* Return the base table this bean type maps to.
*/
String getBaseTable();
/**
* Create a new instance of the bean.
*/
T createBean();
/**
* Return the bean id. This is the same as getBeanId() but without the generic type.
*/
Object beanId(Object bean);
/**
* Return the id value for the given bean.
*/
Object getBeanId(T bean);
/**
* Set the id value to the bean.
*/
void setBeanId(T bean, Object idValue);
/**
* Return the bean persist controller.
*/
BeanPersistController getPersistController();
/**
* Return the bean persist listener.
*/
BeanPersistListener getPersistListener();
/**
* Return the beanFinder. Usually null unless overriding the finder.
*/
BeanFindController getFindController();
/**
* Return the BeanQueryAdapter or null if none is defined.
*/
BeanQueryAdapter getQueryAdapter();
/**
* Return the identity generation type.
*/
IdType getIdType();
/**
* Return the sequence name associated to this entity bean type (if there is one).
*/
String getSequenceName();
/**
* Return true if this bean type has doc store backing.
*/
boolean isDocStoreMapped();
/**
* Return the DocumentMapping for this bean type.
* <p>
* This is the document structure and mapping options for how this bean type is mapped
* for the document store.
* </p>
*/
DocumentMapping getDocMapping();
/**
* Return the doc store queueId for this bean type.
*/
String getDocStoreQueueId();
/**
* Return the doc store support for this bean type.\
*/
BeanDocType<T> docStore();
/**
* Add the discriminator value to the query if needed.
*/
void addInheritanceWhere(SpiQuery<?> query);
/**
* Return the root bean type for an inheritance hierarchy.
*/
BeanType<?> root();
/**
* Return true if this bean type has an inheritance hierarchy.
*/
boolean hasInheritance();
/**
* Return the discriminator column.
*/
String getDiscColumn();
/**
* Create a bean given the discriminator value.
*/
T createBeanUsingDisc(Object discValue);
}
@@ -0,0 +1,57 @@
package com.avaje.ebean.plugin;
import com.avaje.ebean.text.StringParser;
/**
* A dot notation expression path.
*/
public interface ExpressionPath {
/**
* Return true if there is a property on the path that is a many property.
*/
boolean containsMany();
/**
* Return the value from a given entity bean.
*/
Object pathGet(Object bean);
/**
* Set a value to the bean for this expression path.
*
* @param bean the bean to set the value on
* @param value the value to set
*/
void pathSet(Object bean, Object value);
/**
* Convert the value to the expected type.
* <p>
* Typically useful for converting strings to the appropriate number type etc.
* </p>
*/
Object convert(Object value);
/**
* Return the default StringParser for the scalar property.
*/
StringParser getStringParser();
/**
* For DateTime capable scalar types convert the long systemTimeMillis into
* an appropriate java time (Date,Timestamp,Time,Calendar, JODA type etc).
*/
Object parseDateTime(long systemTimeMillis);
/**
* Return true if the last type is "DateTime capable" - can support
* {@link #parseDateTime(long)}.
*/
boolean isDateTimeCapable();
/**
* Return the underlying JDBC type or 0 if this is not a scalar type.
*/
int getJdbcType();
}
@@ -3,7 +3,7 @@ package com.avaje.ebean.plugin;
/**
* A 'plugin' that wants to be configured on startup so it can use features of the EbeanServer itself.
*/
public interface SpiServerPlugin {
public interface Plugin {
/**
* Configure the plugin.
@@ -0,0 +1,22 @@
package com.avaje.ebean.plugin;
/**
* Property of a entity bean that can be read.
*/
public interface Property {
/**
* Return the name of the property.
*/
String getName();
/**
* Return the value of the property on the given bean.
*/
Object getVal(Object bean);
/**
* Return true if this is a OneToMany or ManyToMany property.
*/
boolean isMany();
}
@@ -1,64 +0,0 @@
package com.avaje.ebean.plugin;
import com.avaje.ebean.config.dbplatform.IdType;
import com.avaje.ebean.event.BeanFindController;
import com.avaje.ebean.event.BeanPersistController;
import com.avaje.ebean.event.BeanPersistListener;
import com.avaje.ebean.event.BeanQueryAdapter;
/**
* Information and methods on BeanDescriptors made available to plugins.
*/
public interface SpiBeanType<T> {
/**
* Return the class type this BeanDescriptor describes.
*/
Class<T> getBeanType();
/**
* Return true if the property is a valid known property or path for the given bean type.
*/
boolean isValidExpression(String property);
/**
* Return the base table this bean type maps to.
*/
String getBaseTable();
/**
* Return the id value for the given bean.
*/
Object getBeanId(T bean);
/**
* Return the bean persist controller.
*/
BeanPersistController getPersistController();
/**
* Return the bean persist listener.
*/
BeanPersistListener getPersistListener();
/**
* Return the beanFinder. Usually null unless overriding the finder.
*/
BeanFindController getFindController();
/**
* Return the BeanQueryAdapter or null if none is defined.
*/
BeanQueryAdapter getQueryAdapter();
/**
* Return the identity generation type.
*/
IdType getIdType();
/**
* Return the sequence name associated to this entity bean type (if there is one).
*/
String getSequenceName();
}
@@ -24,16 +24,20 @@ public interface SpiServer extends EbeanServer {
/**
* Return all the bean types registered on this server instance.
*/
List<? extends SpiBeanType<?>> getBeanTypes();
List<? extends BeanType<?>> getBeanTypes();
/**
* Return the bean type for a given entity bean class.
*/
<T> SpiBeanType<T> getBeanType(Class<T> beanClass);
<T> BeanType<T> getBeanType(Class<T> beanClass);
/**
* Return the bean types mapped to the given base table.
*/
List<? extends SpiBeanType<?>> getBeanTypes(String baseTableName);
List<? extends BeanType<?>> getBeanTypes(String baseTableName);
/**
* Return the bean type for a given doc store queueId.
*/
BeanType<?> getBeanTypeForQueueId(String queueId);
}
@@ -0,0 +1,98 @@
package com.avaje.ebean.search;
/**
* Options for the text match and multi match expressions.
*/
public abstract class AbstractMatch {
protected boolean operatorAnd;
protected String analyzer;
protected double boost;
protected String minShouldMatch;
protected int maxExpansions;
protected String zeroTerms;
protected double cutoffFrequency;
protected String fuzziness;
protected int prefixLength;
protected String rewrite;
/**
* Return true if using the AND operator otherwise using the OR operator.
*/
public boolean isOperatorAnd() {
return operatorAnd;
}
/**
* Return the boost.
*/
public double getBoost() {
return boost;
}
/**
* Return the minimum should match.
*/
public String getMinShouldMatch() {
return minShouldMatch;
}
/**
* Return the zero terms option.
*/
public String getZeroTerms() {
return zeroTerms;
}
/**
* Return the cutoff frequency.
*/
public double getCutoffFrequency() {
return cutoffFrequency;
}
/**
* Return the max expansions.
*/
public int getMaxExpansions() {
return maxExpansions;
}
/**
* Return the analyzer.
*/
public String getAnalyzer() {
return analyzer;
}
/**
* Return the fuzziness.
*/
public String getFuzziness() {
return fuzziness;
}
/**
* Return the prefix length.
*/
public int getPrefixLength() {
return prefixLength;
}
/**
* Return the rewrite option.
*/
public String getRewrite() {
return rewrite;
}
}
@@ -0,0 +1,117 @@
package com.avaje.ebean.search;
/**
* Options for the text match expression.
*/
public class Match extends AbstractMatch {
protected boolean phrase;
protected boolean phrasePrefix;
public Match() {
}
/**
* Set this to be a "Phrase" type expression.
*/
public Match phrase() {
phrase = true;
return this;
}
/**
* Set this to be a "Phrase Prefix" type expression.
*/
public Match phrasePrefix() {
phrasePrefix = true;
return this;
}
/**
* Use the AND operator (rather than OR).
*/
public Match opAnd() {
operatorAnd = true;
return this;
}
/**
* Use the OR operator (rather than AND).
*/
public Match opOr() {
operatorAnd = false;
return this;
}
/**
* Set the zero terms.
*/
public Match zeroTerms(String zeroTerms) {
this.zeroTerms = zeroTerms;
return this;
}
/**
* Set the cutoff frequency.
*/
public Match cutoffFrequency(double cutoffFrequency) {
this.cutoffFrequency = cutoffFrequency;
return this;
}
/**
* Set the max expansions (for phrase prefix only).
*/
public Match maxExpansions(int maxExpansions) {
this.maxExpansions = maxExpansions;
return this;
}
/**
* Set the Analyzer to use for this expression.
*/
public Match analyzer(String analyzer) {
this.analyzer = analyzer;
return this;
}
/**
* Set the boost.
*/
public Match boost(double boost) {
this.boost = boost;
return this;
}
/**
* Set the rewrite to use.
*/
public Match minShouldMatch(String minShouldMatch) {
this.minShouldMatch = minShouldMatch;
return this;
}
/**
* Set the rewrite to use.
*/
public Match rewrite(String rewrite) {
this.rewrite = rewrite;
return this;
}
/**
* Return true if this is a phrase query.
*/
public boolean isPhrase() {
return phrase;
}
/**
* Return true if this is a phrase prefix query.
*/
public boolean isPhrasePrefix() {
return phrasePrefix;
}
}
@@ -0,0 +1,148 @@
package com.avaje.ebean.search;
/**
* Options for the text match expression.
*/
public class MultiMatch extends AbstractMatch {
/**
* The MultiMatch type.
*/
public enum Type {
BEST_FIELDS,
MOST_FIELDS,
CROSS_FIELDS,
PHRASE,
PHRASE_PREFIX
}
protected final String[] fields;
protected Type type = Type.BEST_FIELDS;
protected double tieBreaker;
/**
* Create with the given fields.
*/
public static MultiMatch fields(String... fields) {
return new MultiMatch(fields);
}
/**
* Construct with a set of fields.
*/
public MultiMatch(String... fields) {
this.fields = fields;
}
/**
* Set the type of query.
*/
public MultiMatch type(Type type) {
this.type = type;
return this;
}
/**
* Set the tieBreaker to use.
*/
public MultiMatch tieBreaker(double tieBreaker) {
this.tieBreaker = tieBreaker;
return this;
}
/**
* Use the AND operator (rather than OR).
*/
public MultiMatch opAnd() {
operatorAnd = true;
return this;
}
/**
* Use the OR operator (rather than AND).
*/
public MultiMatch opOr() {
operatorAnd = false;
return this;
}
/**
* Set the minimum should match value.
*/
public MultiMatch minShouldMatch(String minShouldMatch) {
this.minShouldMatch = minShouldMatch;
return this;
}
/**
* Set the boost.
*/
public MultiMatch boost(double boost) {
this.boost = boost;
return this;
}
/**
* Set the zero terms.
*/
public MultiMatch zeroTerms(String zeroTerms) {
this.zeroTerms = zeroTerms;
return this;
}
/**
* Set the cutoff frequency.
*/
public MultiMatch cutoffFrequency(double cutoffFrequency) {
this.cutoffFrequency = cutoffFrequency;
return this;
}
/**
* Set the max expansions (for phrase prefix only).
*/
public MultiMatch maxExpansions(int maxExpansions) {
this.maxExpansions = maxExpansions;
return this;
}
/**
* Set the Analyzer to use for this expression.
*/
public MultiMatch analyzer(String analyzer) {
this.analyzer = analyzer;
return this;
}
/**
* Set the rewrite to use.
*/
public MultiMatch rewrite(String rewrite) {
this.rewrite = rewrite;
return this;
}
/**
* Return the type.
*/
public Type getType() {
return type;
}
/**
* Return the fields to search.
*/
public String[] getFields() {
return fields;
}
/**
* Return the tie breaker.
*/
public double getTieBreaker() {
return tieBreaker;
}
}
@@ -0,0 +1,141 @@
package com.avaje.ebean.search;
/**
* Text common terms query.
* <p>
* This maps to an ElasticSearch "common terms query".
* </p>
* <pre>{@code
*
* TextCommonTerms options = new TextCommonTerms()
* .cutoffFrequency(0.001)
* .minShouldMatch("50%")
* .lowFreqOperatorAnd(true)
* .highFreqOperatorAnd(true);
*
* List<Customer> customers = server.find(Customer.class)
* .text()
* .textCommonTerms("the brown", options)
* .findList();
*
* }</pre>
*
* <pre>{@code
*
* // ElasticSearch expression
*
* "common": {
* "body": {
* "query": "the brown",
* "cutoff_frequency": 0.001,
* "low_freq_operator": "and",
* "high_freq_operator": "and",
* "minimum_should_match": "50%"
* }
* }
*
* }</pre>
*/
public class TextCommonTerms {
protected double cutoffFrequency;
protected boolean lowFreqOperatorAnd;
protected boolean highFreqOperatorAnd;
protected String minShouldMatch;
protected String minShouldMatchLowFreq;
protected String minShouldMatchHighFreq;
/**
* Set the cutoff frequency.
*/
public TextCommonTerms cutoffFrequency(double cutoffFrequency) {
this.cutoffFrequency = cutoffFrequency;
return this;
}
/**
* Set to true if low frequency terms should use AND operator.
*/
public TextCommonTerms lowFreqOperatorAnd(boolean opAnd) {
this.lowFreqOperatorAnd = opAnd;
return this;
}
/**
* Set to true if high frequency terms should use AND operator.
*/
public TextCommonTerms highFreqOperatorAnd(boolean opAnd) {
this.highFreqOperatorAnd = opAnd;
return this;
}
/**
* Set the minimum should match.
*/
public TextCommonTerms minShouldMatch(String minShouldMatch) {
this.minShouldMatch = minShouldMatch;
return this;
}
/**
* Set the minimum should match for low frequency terms.
*/
public TextCommonTerms minShouldMatchLowFreq(String minShouldMatchLowFreq) {
this.minShouldMatchLowFreq = minShouldMatchLowFreq;
return this;
}
/**
* Set the minimum should match for high frequency terms.
*/
public TextCommonTerms minShouldMatchHighFreq(String minShouldMatchHighFreq) {
this.minShouldMatchHighFreq = minShouldMatchHighFreq;
return this;
}
/**
* Return true if low freq should use the AND operator.
*/
public boolean isLowFreqOperatorAnd() {
return lowFreqOperatorAnd;
}
/**
* Return true if high freq should use the AND operator.
*/
public boolean isHighFreqOperatorAnd() {
return highFreqOperatorAnd;
}
/**
* Return the cutoff frequency.
*/
public double getCutoffFrequency() {
return cutoffFrequency;
}
/**
* Return the minimum to match.
*/
public String getMinShouldMatch() {
return minShouldMatch;
}
/**
* Return the minimum to match for high frequency.
*/
public String getMinShouldMatchHighFreq() {
return minShouldMatchHighFreq;
}
/**
* Return the minimum to match for low frequency.
*/
public String getMinShouldMatchLowFreq() {
return minShouldMatchLowFreq;
}
}
@@ -0,0 +1,436 @@
package com.avaje.ebean.search;
/**
* Text query string options.
* <p>
* This maps to an ElasticSearch "query string query".
* </p>
*
* <pre>{@code
*
* TextQueryString options = new TextQueryString()
* .analyzeWildcard(true)
* .fields("name")
* .lenient(true)
* .opAnd();
*
* List<Customer> customers = server.find(Customer.class)
* .text()
* .textSimple("quick brown", options)
* .findList();
*
* }</pre>
*
*
* <pre>{@code
*
* // just use default options
* TextQueryString options = new TextQueryString();
*
* List<Customer> customers = server.find(Customer.class)
* .text()
* .textSimple("quick brown", options)
* .findList();
*
* }</pre>
*
*/
public class TextQueryString {
public static final int DEFAULT_FUZZY_MAX_EXPANSIONS = 50;
public static final int DEFAULT_MAX_DETERMINIZED_STATES = 10000;
protected final String[] fields;
/**
* Only used when multiple fields set.
*/
protected boolean useDisMax = true;
/**
* Only used when multiple fields set.
*/
protected double tieBreaker;
protected String defaultField;
protected boolean operatorAnd;
protected String analyzer;
protected boolean allowLeadingWildcard = true;
protected boolean lowercaseExpandedTerms = true;
protected boolean enablePositionIncrements = true;
protected int fuzzyMaxExpansions = DEFAULT_FUZZY_MAX_EXPANSIONS;
protected String fuzziness;
protected int fuzzyPrefixLength;
protected double phraseSlop;
protected double boost;
protected boolean analyzeWildcard;
protected boolean autoGeneratePhraseQueries;
protected int maxDeterminizedStates = DEFAULT_MAX_DETERMINIZED_STATES;
protected String minShouldMatch;
protected boolean lenient;
protected String locale;
protected String timeZone;
protected String rewrite;
/**
* Create with given fields.
*/
public static TextQueryString fields(String... fields) {
return new TextQueryString(fields);
}
/**
* Construct with the fields to use.
*/
public TextQueryString(String... fields) {
this.fields = fields;
}
/**
* Use the AND operator (rather than OR).
*/
public TextQueryString opAnd() {
this.operatorAnd = true;
return this;
}
/**
* Use the OR operator (rather than AND).
*/
public TextQueryString opOr() {
this.operatorAnd = false;
return this;
}
/**
* Set the locale.
*/
public TextQueryString locale(String locale) {
this.locale = locale;
return this;
}
/**
* Set lenient mode.
*/
public TextQueryString lenient(boolean lenient) {
this.lenient = lenient;
return this;
}
/**
* Set the minimum should match.
*/
public TextQueryString minShouldMatch(String minShouldMatch) {
this.minShouldMatch = minShouldMatch;
return this;
}
/**
* Set the analyzer.
*/
public TextQueryString analyzer(String analyzer) {
this.analyzer = analyzer;
return this;
}
/**
* Set useDisMax option (when multiple fields only).
*/
public TextQueryString useDisMax(boolean useDisMax) {
this.useDisMax = useDisMax;
return this;
}
/**
* Set tieBreaker option (when multiple fields only).
*/
public TextQueryString tieBreaker(double tieBreaker) {
this.tieBreaker = tieBreaker;
return this;
}
/**
* Set the default field.
*/
public TextQueryString defaultField(String defaultField) {
this.defaultField = defaultField;
return this;
}
/**
* Set allow leading wildcard mode.
*/
public TextQueryString allowLeadingWildcard(boolean allowLeadingWildcard) {
this.allowLeadingWildcard = allowLeadingWildcard;
return this;
}
/**
* Set lowercase expanded terms mode.
*/
public TextQueryString lowercaseExpandedTerms(boolean lowercaseExpandedTerms) {
this.lowercaseExpandedTerms = lowercaseExpandedTerms;
return this;
}
/**
* Set enable position increments mode.
*/
public TextQueryString enablePositionIncrements(boolean enablePositionIncrements) {
this.enablePositionIncrements = enablePositionIncrements;
return this;
}
/**
* Set fuzzy max expansions.
*/
public TextQueryString fuzzyMaxExpansions(int fuzzyMaxExpansions) {
this.fuzzyMaxExpansions = fuzzyMaxExpansions;
return this;
}
/**
* Set fuzziness.
*/
public TextQueryString fuzziness(String fuzziness) {
this.fuzziness = fuzziness;
return this;
}
/**
* Set the fuzzy prefix length.
*/
public TextQueryString fuzzyPrefixLength(int fuzzyPrefixLength) {
this.fuzzyPrefixLength = fuzzyPrefixLength;
return this;
}
/**
* Set the phrase slop.
*/
public TextQueryString phraseSlop(double phraseSlop) {
this.phraseSlop = phraseSlop;
return this;
}
/**
* Set the boost.
*/
public TextQueryString boost(double boost) {
this.boost = boost;
return this;
}
/**
* Set the analyze wildcard mode.
*/
public TextQueryString analyzeWildcard(boolean analyzeWildcard) {
this.analyzeWildcard = analyzeWildcard;
return this;
}
/**
* Set the auto generate phrase queries mode.
*/
public TextQueryString autoGeneratePhraseQueries(boolean autoGeneratePhraseQueries) {
this.autoGeneratePhraseQueries = autoGeneratePhraseQueries;
return this;
}
/**
* Set the max determinized states.
*/
public TextQueryString maxDeterminizedStates(int maxDeterminizedStates) {
this.maxDeterminizedStates = maxDeterminizedStates;
return this;
}
/**
* Set the time zone.
*/
public TextQueryString timeZone(String timeZone) {
this.timeZone = timeZone;
return this;
}
/**
* Set the rewrite option.
*/
public TextQueryString rewrite(String rewrite) {
this.rewrite = rewrite;
return this;
}
/**
* Return the rewrite option.
*/
public String getRewrite() {
return rewrite;
}
/**
* Return the fields.
*/
public String[] getFields() {
return fields;
}
/**
* Return true if AND is the default operator.
*/
public boolean isOperatorAnd() {
return operatorAnd;
}
/**
* Return the analyzer.
*/
public String getAnalyzer() {
return analyzer;
}
/**
* Return the locale.
*/
public String getLocale() {
return locale;
}
/**
* Return lenient mode.
*/
public boolean isLenient() {
return lenient;
}
/**
* Return the minimum should match.
*/
public String getMinShouldMatch() {
return minShouldMatch;
}
/**
* Return the useDixMax mode.
*/
public boolean isUseDisMax() {
return useDisMax;
}
/**
* Return the tie breaker.
*/
public double getTieBreaker() {
return tieBreaker;
}
/**
* Return the default field.
*/
public String getDefaultField() {
return defaultField;
}
/**
* Return the allow leading wildcard mode.
*/
public boolean isAllowLeadingWildcard() {
return allowLeadingWildcard;
}
/**
* Return the lowercase expanded terms mode.
*/
public boolean isLowercaseExpandedTerms() {
return lowercaseExpandedTerms;
}
/**
* Return the enable position increments mode.
*/
public boolean isEnablePositionIncrements() {
return enablePositionIncrements;
}
/**
* Return the fuzzy max expansions.
*/
public int getFuzzyMaxExpansions() {
return fuzzyMaxExpansions;
}
/**
* Return the fuzziness.
*/
public String getFuzziness() {
return fuzziness;
}
/**
* Return the fuzzy prefix length.
*/
public int getFuzzyPrefixLength() {
return fuzzyPrefixLength;
}
/**
* Return the phrase slop.
*/
public double getPhraseSlop() {
return phraseSlop;
}
/**
* Return the analyze wildcard mode.
*/
public boolean isAnalyzeWildcard() {
return analyzeWildcard;
}
/**
* Return the boost.
*/
public double getBoost() {
return boost;
}
/**
* Return the auto generate phase queries mode.
*/
public boolean isAutoGeneratePhraseQueries() {
return autoGeneratePhraseQueries;
}
/**
* Return the max determinized states.
*/
public int getMaxDeterminizedStates() {
return maxDeterminizedStates;
}
/**
* Return the time zone.
*/
public String getTimeZone() {
return timeZone;
}
}
@@ -0,0 +1,194 @@
package com.avaje.ebean.search;
/**
* Simple text query options.
* <p>
* This maps to an ElasticSearch "simple text query".
* </p>
*
* <pre>{@code
*
* TextSimple options = new TextSimple()
* .analyzeWildcard(true)
* .fields("name")
* .lenient(true)
* .opAnd();
*
* List<Customer> customers = server.find(Customer.class)
* .text()
* .textSimple("quick brown", options)
* .findList();
*
* }</pre>
*/
public class TextSimple {
protected String[] fields;
protected boolean operatorAnd;
protected String analyzer;
protected String flags;
protected boolean lowercaseExpandedTerms = true;
protected boolean analyzeWildcard;
protected String locale;
protected boolean lenient;
protected String minShouldMatch;
/**
* Construct
*/
public TextSimple() {
}
/**
* Set the fields.
*/
public TextSimple fields(String... fields) {
this.fields = fields;
return this;
}
/**
* Use AND as the default operator.
*/
public TextSimple opAnd() {
this.operatorAnd = true;
return this;
}
/**
* Use OR as the default operator.
*/
public TextSimple opOr() {
this.operatorAnd = false;
return this;
}
/**
* Set the analyzer
*/
public TextSimple analyzer(String analyzer) {
this.analyzer = analyzer;
return this;
}
/**
* Set the flags.
*/
public TextSimple flags(String flags) {
this.flags = flags;
return this;
}
/**
* Set the false to not use lowercase expanded terms.
*/
public TextSimple lowercaseExpandedTerms(boolean lowercaseExpandedTerms) {
this.lowercaseExpandedTerms = lowercaseExpandedTerms;
return this;
}
/**
* Set to true to use analyze wildcard.
*/
public TextSimple analyzeWildcard(boolean analyzeWildcard) {
this.analyzeWildcard = analyzeWildcard;
return this;
}
/**
* Set the locale.
*/
public TextSimple locale(String locale) {
this.locale = locale;
return this;
}
/**
* Set the lenient mode.
*/
public TextSimple lenient(boolean lenient) {
this.lenient = lenient;
return this;
}
/**
* Set the minimum should match.
*/
public TextSimple minShouldMatch(String minShouldMatch) {
this.minShouldMatch = minShouldMatch;
return this;
}
/**
* Return lenient mode.
*/
public boolean isLenient() {
return lenient;
}
/**
* Return true to analyse wildcard.
*/
public boolean isAnalyzeWildcard() {
return analyzeWildcard;
}
/**
* Return lowercase expanded terms mode.
*/
public boolean isLowercaseExpandedTerms() {
return lowercaseExpandedTerms;
}
/**
* Return true if the default operator should be AND.
*/
public boolean isOperatorAnd() {
return operatorAnd;
}
/**
* Return the analyzer to use.
*/
public String getAnalyzer() {
return analyzer;
}
/**
* Return the fields.
*/
public String[] getFields() {
return fields;
}
/**
* Return the locale.
*/
public String getLocale() {
return locale;
}
/**
* Return the flags.
*/
public String getFlags() {
return flags;
}
/**
* Return the minimum should match.
*/
public String getMinShouldMatch() {
return minShouldMatch;
}
}
@@ -1,14 +1,16 @@
package com.avaje.ebean.text;
import com.avaje.ebean.FetchPath;
import com.avaje.ebean.Query;
import com.avaje.ebeaninternal.server.query.SplitName;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import com.avaje.ebean.Query;
import java.util.Set;
/**
* This is a Tree like structure of paths and properties that can be used for
@@ -20,7 +22,7 @@ import com.avaje.ebean.Query;
* render (JAX-RS JSON / XML).
* </p>
*/
public class PathProperties {
public class PathProperties implements FetchPath {
private final Map<String, Props> pathMap;
@@ -44,39 +46,6 @@ public class PathProperties {
this.pathMap.put(null, rootProps);
}
/**
* Construct for creating copy.
*/
private PathProperties(PathProperties orig) {
this.rootProps = orig.rootProps.copy(this);
this.pathMap = new LinkedHashMap<String, Props>(orig.pathMap.size());
Set<Entry<String, Props>> entrySet = orig.pathMap.entrySet();
for (Entry<String, Props> e : entrySet) {
pathMap.put(e.getKey(), e.getValue().copy(this));
}
}
/**
* Create a copy of this instance so that it can be modified.
* <p>
* For example, you may want to create a copy to add extra properties to a
* path so that they are fetching in a ORM query but perhaps not rendered by
* default. That is, use a PathProperties for JSON or XML rendering, but
* create a copy, add some extra properties and then use that copy to define
* an ORM query.
* </p>
*/
public PathProperties copy() {
return new PathProperties(this);
}
/**
* Return true if there are no paths defined.
*/
public boolean isEmpty() {
return pathMap.isEmpty();
}
public String toString() {
return pathMap.toString();
}
@@ -84,6 +53,7 @@ public class PathProperties {
/**
* Return true if the path is defined and has properties.
*/
@Override
public boolean hasPath(String path) {
Props props = pathMap.get(path);
return props != null && !props.isEmpty();
@@ -92,40 +62,38 @@ public class PathProperties {
/**
* Get the properties for a given path.
*/
public Set<String> get(String path) {
@Override
public Set<String> getProperties(String path) {
Props props = pathMap.get(path);
return props == null ? null : props.getProperties();
}
public void addToPath(String path, String property) {
getProps(path).getProperties().add(property);
}
public void addNested(String prefix, PathProperties pathProps) {
for (Entry<String, Props> entry : pathProps.pathMap.entrySet()) {
String path = pathAdd(prefix, entry.getKey());
String[] split = SplitName.split(path);
getProps(split[0]).addProperty(split[1]);
getProps(path).addProps(entry.getValue());
}
}
private String pathAdd(String prefix, String key) {
return key == null ? prefix : prefix + "." + key;
}
Props getProps(String path) {
Props props = pathMap.get(path);
if (props == null) {
props = new Props(this, null, path);
pathMap.put(path, props);
}
props.getProperties().add(property);
}
/**
* Set the properties for a given path.
*/
public void put(String path, Set<String> properties) {
pathMap.put(path, new Props(this, null, path, properties));
}
/**
* Remove a path returning the properties set for that path.
*/
public Set<String> remove(String path) {
Props props = pathMap.remove(path);
return props == null ? null : props.getProperties();
}
/**
* Return a shallow copy of the paths.
*/
public Set<String> getPaths() {
return new LinkedHashSet<String>(pathMap.keySet());
return props;
}
public Collection<Props> getPathProps() {
@@ -135,7 +103,7 @@ public class PathProperties {
/**
* Apply these path properties as fetch paths to the query.
*/
public void apply(Query<?> query) {
public <T> void apply(Query<T> query) {
for (Entry<String, Props> entry : pathMap.entrySet()) {
String path = entry.getKey();
@@ -153,6 +121,40 @@ public class PathProperties {
return rootProps;
}
/**
* Return true if the property (dot notation) is included in the PathProperties.
*/
public boolean includesProperty(String name) {
String[] split = SplitName.split(name);
Props props = pathMap.get(split[0]);
return (props != null && props.includes(split[1]));
}
/**
* Return true if the property is included using a prefix.
*/
public boolean includesProperty(String prefix, String name) {
return includesProperty(SplitName.add(prefix, name));
}
/**
* Return true if the fetch path is included in the PathProperties.
* <p>
* The fetch path is a OneToMany or ManyToMany path in dot notation.
* </p>
*/
public boolean includesPath(String path) {
return pathMap.containsKey(path);
}
/**
* Return true if the path is included using a prefix.
*/
public boolean includesPath(String prefix, String name) {
return includesPath(SplitName.add(prefix, name));
}
public static class Props {
private final PathProperties owner;
@@ -160,9 +162,9 @@ public class PathProperties {
private final String parentPath;
private final String path;
private final Set<String> propSet;
private final LinkedHashSet<String> propSet;
private Props(PathProperties owner, String parentPath, String path, Set<String> propSet) {
private Props(PathProperties owner, String parentPath, String path, LinkedHashSet<String> propSet) {
this.owner = owner;
this.path = path;
this.parentPath = parentPath;
@@ -173,13 +175,6 @@ public class PathProperties {
this(owner, parentPath, path, new LinkedHashSet<String>());
}
/**
* Create a shallow copy of this Props instance.
*/
public Props copy(PathProperties newOwner) {
return new Props(newOwner, parentPath, path, new LinkedHashSet<String>(propSet));
}
public String getPath() {
return path;
}
@@ -195,7 +190,7 @@ public class PathProperties {
/**
* Return the properties for this property set.
*/
public Set<String> getProperties() {
public LinkedHashSet<String> getProperties() {
return propSet;
}
@@ -228,15 +223,15 @@ public class PathProperties {
/**
* Add a child Property set.
*/
protected Props addChild(String subpath) {
protected Props addChild(String subPath) {
subpath = subpath.trim();
addProperty(subpath);
subPath = subPath.trim();
addProperty(subPath);
// build the subpath
String p = path == null ? subpath : path + "." + subpath;
Props nested = new Props(owner, path, p);
owner.pathMap.put(p, nested);
// build the subPath
String fullPath = path == null ? subPath : path + "." + subPath;
Props nested = new Props(owner, path, fullPath);
owner.pathMap.put(fullPath, nested);
return nested;
}
@@ -246,6 +241,14 @@ public class PathProperties {
protected void addProperty(String property) {
propSet.add(property.trim());
}
private void addProps(Props value) {
propSet.addAll(value.propSet);
}
private boolean includes(String prop) {
return propSet.isEmpty() || propSet.contains(prop) || propSet.contains("*");
}
}
}
@@ -3,7 +3,7 @@ package com.avaje.ebean.text;
/**
* Parses Uri segments like :(id,name,shippingAddress(*),contacts(*)) so that
* the response can be customised for performance.
*
*
* @author rbygrave
*/
class PathPropertiesParser {
@@ -50,12 +50,12 @@ class PathPropertiesParser {
do {
char c1 = chars[pos++];
switch (c1) {
case '(':
return currentWord();
default:
if (pos == 1) {
return "";
}
case '(':
return currentWord();
default:
if (pos == 1) {
return "";
}
}
} while (pos < eof);
throw new RuntimeException("Hit EOF while reading sectionTitle from " + startPos);
@@ -72,28 +72,30 @@ class PathPropertiesParser {
}
private void parseSection() {
do {
char c1 = chars[pos++];
switch (c1) {
case '(':
addSubpath();
break;
case ',':
addCurrentProperty();
break;
case ':':
// start new section
startPos = pos;
return;
case ')':
// end of section
addCurrentProperty();
popSubpath();
break;
default:
}
if (pos < eof) {
do {
char c1 = chars[pos++];
switch (c1) {
case '(':
addSubpath();
break;
case ',':
addCurrentProperty();
break;
case ':':
// start new section
startPos = pos;
return;
case ')':
// end of section
addCurrentProperty();
popSubpath();
break;
default:
}
} while (pos < eof);
} while (pos < eof);
}
if (startPos < pos) {
String currentWord = source.substring(startPos, pos);
currentPathProps.addProperty(currentWord);
@@ -0,0 +1,35 @@
package com.avaje.ebean.text.json;
import com.avaje.ebean.bean.PersistenceContext;
import com.fasterxml.jackson.core.JsonParser;
/**
* Provides a JSON reader that can hold a persistence context and load context while reading JSON.
* <p>
* This provides a mechanism such that an object loaded from JSON can have unique instances
* by using a persistence context and also support further lazy loading (via a load context).
* </p>
*/
public interface JsonBeanReader<T> {
/**
* Read the JSON returning a bean.
*/
T read();
/**
* Create a new reader taking the context from the existing one but using a new JsonParser.
*/
JsonBeanReader<T> forJson(JsonParser moreJson, boolean resetContext);
/**
* Add a bean explicitly to the persistence context.
*/
void persistenceContextPut(Object beanId, T currentBean);
/**
* Return the persistence context if one is being used.
*/
PersistenceContext getPersistenceContext();
}
@@ -1,9 +1,11 @@
package com.avaje.ebean.text.json;
import com.avaje.ebean.text.PathProperties;
import com.avaje.ebean.FetchPath;
import com.avaje.ebean.plugin.BeanType;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
@@ -56,6 +58,26 @@ public interface JsonContext {
*/
<T> T toBean(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException;
/**
* Create and return a new bean reading for the bean type given the JSON options and source.
* <p>
* Note that JsonOption provides an option for setting a persistence context and also enabling
* further lazy loading. Further lazy loading requires a persistence context so if that is set
* on then a persistence context is created if there is not one set.
* </p>
*/
<T> JsonBeanReader<T> createBeanReader(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException;
/**
* Create and return a new bean reading for the bean type given the JSON options and source.
* <p>
* Note that JsonOption provides an option for setting a persistence context and also enabling
* further lazy loading. Further lazy loading requires a persistence context so if that is set
* on then a persistence context is created if there is not one set.
* </p>
*/
<T> JsonBeanReader<T> createBeanReader(BeanType<T> beanType, JsonParser parser, JsonReadOptions options) throws JsonIOException;
/**
* Convert json string input into a list of beans of a specific type.
*
@@ -144,21 +166,21 @@ public interface JsonContext {
void toJson(Object value, JsonGenerator generator) throws JsonIOException;
/**
* Return the bean or collection as JSON string using PathProperties.
* Return the bean or collection as JSON string using FetchPath.
*
* @throws JsonIOException When IOException occurs
*/
String toJson(Object value, PathProperties pathProperties) throws JsonIOException;
String toJson(Object value, FetchPath fetchPath) throws JsonIOException;
/**
* Write the bean or collection as json to the writer using the PathProperties.
* Write the bean or collection as json to the writer using the FetchPath.
*/
void toJson(Object value, Writer writer, PathProperties pathProperties) throws JsonIOException;
void toJson(Object value, Writer writer, FetchPath fetchPath) throws JsonIOException;
/**
* Write the bean or collection to the JsonGenerator using the PathProperties.
* Write the bean or collection to the JsonGenerator using the FetchPath.
*/
void toJson(Object value, JsonGenerator generator, PathProperties pathProperties) throws JsonIOException;
void toJson(Object value, JsonGenerator generator, FetchPath fetchPath) throws JsonIOException;
/**
* Deprecated in favour of using PathProperties by itself.
@@ -203,13 +225,13 @@ public interface JsonContext {
JsonParser createParser(Reader reader) throws JsonIOException;
/**
* Return a helper that can write scalar types known to Ebean to Jackson.
* Write a scalar types known to Ebean to Jackson.
* <p>
* Ebean has built in support for java8 and Joda types as well as the other
* standard JDK types like URI, URL, UUID etc. This is a fast simple way to
* write any of those types to Jackson.
* </p>
*/
JsonScalar getScalar(JsonGenerator generator);
void writeScalar(JsonGenerator generator, Object scalarValue) throws IOException;
}
@@ -1,5 +1,7 @@
package com.avaje.ebean.text.json;
import com.avaje.ebean.bean.PersistenceContext;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -17,6 +19,12 @@ public class JsonReadOptions {
protected Object objectMapper;
protected boolean enableLazyLoading;
protected PersistenceContext persistenceContext;
protected Object loadContext;
/**
* Default constructor.
*/
@@ -46,6 +54,25 @@ public class JsonReadOptions {
return this;
}
/**
* Return true if lazy loading is enabled after the objects are loaded.
*/
public boolean isEnableLazyLoading() {
return enableLazyLoading;
}
/**
* Set to true to enable lazy loading on partially populated beans.
* <p>
* If this is set to true a persistence context will be created if one has
* not already been supplied.
* </p>
*/
public JsonReadOptions setEnableLazyLoading(boolean enableLazyLoading) {
this.enableLazyLoading = enableLazyLoading;
return this;
}
/**
* Return the Jackson ObjectMapper to use (if not wanted to use the objectMapper set on the ServerConfig).
*/
@@ -56,7 +83,38 @@ public class JsonReadOptions {
/**
* Set the Jackson ObjectMapper to use (if not wanted to use the objectMapper set on the ServerConfig).
*/
public void setObjectMapper(Object objectMapper) {
public JsonReadOptions setObjectMapper(Object objectMapper) {
this.objectMapper = objectMapper;
return this;
}
/**
* Set the persistence context to use when building the object graph from the JSON.
*/
public JsonReadOptions setPersistenceContext(PersistenceContext persistenceContext) {
this.persistenceContext = persistenceContext;
return this;
}
/**
* Return the persistence context to use when marshalling JSON.
*/
public PersistenceContext getPersistenceContext() {
return persistenceContext;
}
/**
* Return the load context to use.
*/
public Object getLoadContext() {
return loadContext;
}
/**
* Set the load context to use.
*/
public void setLoadContext(Object loadContext) {
this.loadContext = loadContext;
}
}
@@ -1,11 +0,0 @@
package com.avaje.ebean.text.json;
import java.io.IOException;
/**
* Writes any scalar type known to Ebean the Jackson generator.
*/
public interface JsonScalar {
void write(String name, Object value) throws IOException;
}
@@ -1,5 +1,6 @@
package com.avaje.ebean.text.json;
import com.avaje.ebean.FetchPath;
import com.avaje.ebean.config.JsonConfig;
import com.avaje.ebean.text.PathProperties;
@@ -15,7 +16,7 @@ import java.util.Map;
*/
public class JsonWriteOptions {
protected PathProperties pathProperties;
protected FetchPath pathProperties;
protected Object objectMapper;
@@ -38,7 +39,7 @@ public class JsonWriteOptions {
/**
* Construct JsonWriteOptions with the given pathProperties.
*/
public static JsonWriteOptions pathProperties(PathProperties pathProperties) {
public static JsonWriteOptions pathProperties(FetchPath pathProperties) {
JsonWriteOptions o = new JsonWriteOptions();
o.setPathProperties(pathProperties);
return o;
@@ -47,14 +48,14 @@ public class JsonWriteOptions {
/**
* Set the Map of properties to include by path.
*/
public void setPathProperties(PathProperties pathProperties) {
public void setPathProperties(FetchPath pathProperties) {
this.pathProperties = pathProperties;
}
/**
* Return the properties to include by path.
*/
public PathProperties getPathProperties() {
public FetchPath getPathProperties() {
return pathProperties;
}
@@ -135,6 +135,11 @@ public interface JsonWriter {
*/
void writeNumber(long value);
/**
* Write a double value.
*/
void writeNumber(double value);
/**
* Write a BigDecimal value (typically inside a list).
*/
@@ -0,0 +1,14 @@
package com.avaje.ebeaninternal.api;
/**
* Key used for caching query plans for ORM and RawSql queries.
*/
public interface CQueryPlanKey {
/**
* Used by read audit such that we can log read audit entries without the full sql
* (which would make the read audit logs verbose).
*/
String getPartialKey();
}
@@ -5,32 +5,18 @@ package com.avaje.ebeaninternal.api;
*/
public class HashQuery {
private final HashQueryPlan planHash;
private final CQueryPlanKey planHash;
private final int bindHash;
/**
* Create the HashQuery.
*/
public HashQuery(HashQueryPlan planHash, int bindHash) {
public HashQuery(CQueryPlanKey planHash, int bindHash) {
this.planHash = planHash;
this.bindHash = bindHash;
}
/**
* Return the query plan hash.
*/
public HashQueryPlan getPlanHash() {
return planHash;
}
/**
* Return the bind values hash.
*/
public int getBindHash() {
return bindHash;
}
public int hashCode() {
int hc = 31 * planHash.hashCode();
hc = 31 * hc + bindHash;
@@ -1,5 +1,7 @@
package com.avaje.ebeaninternal.api;
import java.util.Set;
/**
* Used to build HashQueryPlan instances.
*/
@@ -9,21 +11,19 @@ public class HashQueryPlanBuilder {
private int bindCount;
private String rawSql;
public HashQueryPlanBuilder() {
this.planHash = 31;
this.planHash = 92821;
}
public String toString() {
return planHash+":"+bindCount+(rawSql != null ? ":r" : "");
return planHash+":"+bindCount;
}
/**
* Add a class to the hash calculation.
*/
public HashQueryPlanBuilder add(Class<?> cls) {
planHash = planHash * 31 + cls.getName().hashCode();
planHash = planHash * 92821 + cls.getName().hashCode();
return this;
}
@@ -31,7 +31,22 @@ public class HashQueryPlanBuilder {
* Add an object to the hash calculation.
*/
public HashQueryPlanBuilder add(Object object) {
planHash = planHash * 31 + (object == null ? 0 : object.hashCode());
planHash = planHash * 92821 + (object == null ? 0 : object.hashCode());
return this;
}
/**
* Add the set with order being important.
*/
public HashQueryPlanBuilder addOrdered(Set<?> set) {
if (set == null) {
add(false);
} else {
add(true);
for (Object o : set) {
add(o);
}
}
return this;
}
@@ -39,7 +54,7 @@ public class HashQueryPlanBuilder {
* Add an integer to the hash calculation.
*/
public HashQueryPlanBuilder add(int hashValue) {
planHash = planHash * 31 + (hashValue);
planHash = planHash * 92821 + (hashValue);
return this;
}
@@ -47,7 +62,7 @@ public class HashQueryPlanBuilder {
* Add a boolean to the hash calculation.
*/
public HashQueryPlanBuilder add(boolean booleanValue) {
planHash = planHash * 31 + (booleanValue ? 31 : 0);
planHash = planHash * 92821 + (booleanValue ? 92821 : 0);
return this;
}
@@ -58,19 +73,24 @@ public class HashQueryPlanBuilder {
bindCount += extraBindCount;
}
/**
* Add raw sql to the hash.
*/
public void addRawSql(String rawSql) {
this.rawSql = rawSql;
public void bindIfNotNull(Object someValue) {
if (someValue != null) {
bindCount++;
}
}
/**
* Build and return the calculated HashQueryPlan.
*/
public HashQueryPlan build() {
return new HashQueryPlan(rawSql, planHash, bindCount);
public String build() {
return planHash+"_"+bindCount;
}
public int getPlanHash() {
return planHash;
}
public int getBindCount() {
return bindCount;
}
}
@@ -8,7 +8,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Request for loading ManyToOne and OneToOne relationships.
@@ -21,8 +23,6 @@ public class LoadBeanRequest extends LoadRequest {
private final LoadBeanBuffer loadBuffer;
private final int lazyLoadPropertyIndex;
private final String lazyLoadProperty;
private final boolean loadCache;
@@ -30,24 +30,23 @@ public class LoadBeanRequest extends LoadRequest {
/**
* Construct for lazy load request.
*/
public LoadBeanRequest(LoadBeanBuffer LoadBuffer, int lazyLoadPropertyIndex, String lazyLoadProperty, boolean loadCache) {
this(LoadBuffer, null, true, lazyLoadPropertyIndex, lazyLoadProperty, loadCache);
public LoadBeanRequest(LoadBeanBuffer LoadBuffer, String lazyLoadProperty, boolean loadCache) {
this(LoadBuffer, null, true, lazyLoadProperty, loadCache);
}
/**
* Construct for secondary query.
*/
public LoadBeanRequest(LoadBeanBuffer LoadBuffer, OrmQueryRequest<?> parentRequest) {
this(LoadBuffer, parentRequest, false, -1, null, false);
this(LoadBuffer, parentRequest, false, null, false);
}
private LoadBeanRequest(LoadBeanBuffer loadBuffer, OrmQueryRequest<?> parentRequest, boolean lazy,
int lazyLoadPropertyIndex, String lazyLoadProperty, boolean loadCache) {
String lazyLoadProperty, boolean loadCache) {
super(parentRequest, lazy);
this.loadBuffer = loadBuffer;
this.batch = loadBuffer.getBatch();
this.lazyLoadPropertyIndex = lazyLoadPropertyIndex;
this.lazyLoadProperty = lazyLoadProperty;
this.loadCache = loadCache;
}
@@ -96,7 +95,7 @@ public class LoadBeanRequest extends LoadRequest {
*/
public List<Object> getIdList(int batchSize) {
ArrayList<Object> idList = new ArrayList<Object>(batchSize);
List<Object> idList = new ArrayList<Object>(batchSize);
BeanDescriptor<?> desc = loadBuffer.getBeanDescriptor();
for (int i = 0; i < batch.size(); i++) {
@@ -124,7 +123,7 @@ public class LoadBeanRequest extends LoadRequest {
/**
* Configure the query for lazy loading execution.
*/
public void configureQuery(SpiQuery<?> query) {
public void configureQuery(SpiQuery<?> query, List<Object> idList) {
query.setMode(SpiQuery.Mode.LAZYLOAD_BEAN);
query.setPersistenceContext(loadBuffer.getPersistenceContext());
@@ -137,7 +136,13 @@ public class LoadBeanRequest extends LoadRequest {
query.setLazyLoadBatchSize(getBatchSize());
}
loadBuffer.configureQuery(query, getLazyLoadProperty());
loadBuffer.configureQuery(query, lazyLoadProperty);
if (idList.size() == 1) {
query.where().idEq(idList.get(0));
} else {
query.where().idIn(idList);
}
}
/**
@@ -145,25 +150,27 @@ public class LoadBeanRequest extends LoadRequest {
*/
public void postLoad(List<?> list) {
if (isLoadCache()) {
BeanDescriptor<?> desc = loadBuffer.getBeanDescriptor();
for (int i = 0; i < list.size(); i++) {
desc.cacheBeanPutData((EntityBean) list.get(i));
Set<Object> loadedIds = new HashSet<Object>();
BeanDescriptor<?> desc = loadBuffer.getBeanDescriptor();
// collect Ids and maybe load bean cache
for (int i = 0; i < list.size(); i++) {
EntityBean loadedBean = (EntityBean) list.get(i);
loadedIds.add(desc.getId(loadedBean));
if (isLoadCache()) {
desc.cacheBeanPutData(loadedBean);
}
}
if (lazyLoadPropertyIndex > -1) {
// this is a lazy loading query so check for lazy loading failure (due to deleted rows)
if (lazyLoadProperty != null) {
for (int i = 0; i < batch.size(); i++) {
// check if the underlying row in DB was deleted. Mark the bean as 'failed' if
// necessary but allow processing to continue until it is accessed by client code
EntityBeanIntercept ebi = batch.get(i);
// all beans in the batch should have this property loaded now
if (ebi.isLazyLoadFailure(lazyLoadPropertyIndex)) {
BeanDescriptor<?> desc = loadBuffer.getBeanDescriptor();
Object beanId = desc.getId(ebi.getOwner());
ebi.setOwnerId(beanId);
logger.info("Lazy loading unsuccessful for type:" + desc.getName() + " id:" + beanId + " - expecting when bean has been deleted");
Object id = desc.getId(ebi.getOwner());
if (!loadedIds.contains(id)) {
logger.info("Lazy loading unsuccessful for type:" + desc.getName() + " id:" + id + " - expecting when bean has been deleted");
ebi.setLazyLoadFailure(id);
}
}
}
@@ -16,48 +16,40 @@ public interface LoadContext {
*/
int getSecondaryQueriesMinBatchSize(int defaultQueryBatch);
/**
* Execute any secondary (+query) queries if there are any defined.
* @param parentRequest the originating query request
*/
void executeSecondaryQueries(OrmQueryRequest<?> parentRequest);
/**
* Execute any secondary (+query) queries if there are any defined.
*
* @param parentRequest the originating query request
* @param forEach set true when using findEach iteration
*/
void executeSecondaryQueries(OrmQueryRequest<?> parentRequest, boolean forEach);
/**
* Register any secondary queries (+query or +lazy) with their
* appropriate LoadBeanContext or LoadManyContext.
* <p>
* This is so the LoadBeanContext or LoadManyContext use the
* defined query for +query and +lazy execution.
* </p>
*/
void registerSecondaryQueries(SpiQuery<?> query);
/**
* Return the node for a given path which is used by AutoTune profiling.
*/
ObjectGraphNode getObjectGraphNode(String path);
/**
* Return the node for a given path which is used by AutoTune profiling.
*/
ObjectGraphNode getObjectGraphNode(String path);
/**
* Return the persistence context used by this query and future lazy loading.
*/
PersistenceContext getPersistenceContext();
/**
* Return the persistence context used by this query and future lazy loading.
*/
PersistenceContext getPersistenceContext();
/**
* Set the persistence context used by this query and future lazy loading.
* <p>
* Used by query iterator when processing large result sets.
* </p>
*/
void resetPersistenceContext(PersistenceContext persistenceContext);
/**
* Set the persistence context used by this query and future lazy loading.
* <p>
* Used by query iterator when processing large result sets.
* </p>
*/
void resetPersistenceContext(PersistenceContext persistenceContext);
/**
* Register a Bean for lazy loading.
*/
void register(String path, EntityBeanIntercept ebi);
/**
* Register a Bean for lazy loading.
*/
void register(String path, EntityBeanIntercept ebi);
/**
* Register a collection for lazy loading.
*/
void register(String path, BeanCollection<?> bc);
/**
* Register a collection for lazy loading.
*/
void register(String path, BeanCollection<?> bc);
}
@@ -29,4 +29,5 @@ public interface LoadManyBuffer {
void configureQuery(SpiQuery<?> query);
boolean isUseDocStore();
}

Some files were not shown because too many files have changed in this diff Show More