From d55aff3dac1ccdc78b7f8650e52bc5aed62e7a9f Mon Sep 17 00:00:00 2001 From: James Daugherty Date: Mon, 11 May 2026 16:56:55 -0400 Subject: [PATCH 01/74] stage: clone hibernate5 modules to hibernate7 as baseline Copy all source, test examples, BOMs, and build config from the hibernate5 namespace to hibernate7 so that the real hibernate7 PR can be reviewed as a true delta rather than a sea of new files. Modules added: - grails-hibernate7-bom (copy of grails-hibernate5-bom) - grails-data-hibernate7-core, spring-orm, grails-plugin, dbmigration, spring-boot, docs - grails-test-examples/hibernate7 (12 projects mirroring hibernate5) - gradle/hibernate7-test-config.gradle (skipHibernate7Tests flag) Build infrastructure: - publish-root-config.gradle: register hibernate7 modules for publishing - SbomPlugin.groovy: add LGPL exemptions for hibernate5 artifacts used by hibernate7 staging modules - settings.gradle: include all hibernate7 projects --- .../apache/grails/buildsrc/SbomPlugin.groovy | 20 + gradle/hibernate7-test-config.gradle | 74 + gradle/publish-root-config.gradle | 7 + grails-bom/hibernate7/build.gradle | 214 + .../boot-plugin/build.gradle | 68 + .../HibernateGormAutoConfiguration.groovy | 136 + .../GormCompilerAutoConfiguration.groovy | 52 + ...ils.cli.compiler.CompilerAutoConfiguration | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../HibernateGormAutoConfigurationSpec.groovy | 91 + .../GroovyBeanDefinitionReaderSpec.groovy | 57 + grails-data-hibernate7/core/build.gradle | 96 + .../grails/orm/HibernateCriteriaBuilder.java | 293 ++ .../groovy/grails/orm/PagedResultList.java | 98 + .../groovy/grails/orm/RlikeExpression.java | 93 + .../orm/hibernate/HibernateEntity.groovy | 88 + .../hibernate/annotation/ManagedEntity.java | 34 + .../hibernate/mapping/MappingBuilder.groovy | 79 + .../hibernate/AbstractHibernateDatastore.java | 444 ++ .../AbstractHibernateGormInstanceApi.groovy | 491 +++ .../AbstractHibernateGormStaticApi.groovy | 903 +++++ .../AbstractHibernateGormValidationApi.groovy | 169 + .../hibernate/AbstractHibernateSession.java | 208 + .../hibernate/EventListenerIntegrator.java | 155 + .../hibernate/GrailsHibernateTemplate.java | 784 ++++ .../GrailsHibernateTransactionManager.groovy | 108 + .../orm/hibernate/GrailsSessionContext.java | 240 ++ .../orm/hibernate/HibernateDatastore.java | 690 ++++ .../hibernate/HibernateEventListeners.java | 34 + .../hibernate/HibernateGormEnhancer.groovy | 80 + .../hibernate/HibernateGormInstanceApi.groovy | 169 + .../hibernate/HibernateGormStaticApi.groovy | 266 ++ .../HibernateGormValidationApi.groovy | 50 + ...rnateMappingContextSessionFactoryBean.java | 567 +++ .../orm/hibernate/HibernateSession.java | 218 + .../orm/hibernate/IHibernateTemplate.java | 78 + .../orm/hibernate/InstanceApiHelper.java | 54 + .../orm/hibernate/MetadataIntegrator.groovy | 43 + .../orm/hibernate/SessionFactoryHolder.java | 43 + .../access/TraitPropertyAccessStrategy.java | 114 + .../cfg/AbstractGrailsDomainBinder.java | 81 + .../orm/hibernate/cfg/CacheConfig.groovy | 90 + .../orm/hibernate/cfg/ColumnConfig.groovy | 138 + .../hibernate/cfg/CompositeIdentity.groovy | 47 + .../hibernate/cfg/DiscriminatorConfig.groovy | 82 + .../orm/hibernate/cfg/GrailsDomainBinder.java | 3607 +++++++++++++++++ .../hibernate/cfg/GrailsHibernateUtil.java | 464 +++ .../cfg/GrailsIdentifierGeneratorFactory.java | 51 + .../cfg/HibernateMappingBuilder.groovy | 701 ++++ .../cfg/HibernateMappingContext.java | 324 ++ .../HibernateMappingContextConfiguration.java | 378 ++ .../cfg/HibernatePersistentEntity.java | 64 + .../grails/orm/hibernate/cfg/Identity.groovy | 114 + .../orm/hibernate/cfg/IdentityEnumType.java | 203 + .../orm/hibernate/cfg/InstanceProxy.groovy | 79 + .../grails/orm/hibernate/cfg/JoinTable.groovy | 85 + .../grails/orm/hibernate/cfg/Mapping.groovy | 589 +++ .../grails/orm/hibernate/cfg/NaturalId.groovy | 41 + .../cfg/PersistentEntityNamingStrategy.java | 34 + .../orm/hibernate/cfg/PropertyConfig.groovy | 476 +++ .../cfg/PropertyDefinitionDelegate.groovy | 77 + .../grails/orm/hibernate/cfg/Settings.java | 30 + .../orm/hibernate/cfg/SortConfig.groovy | 52 + .../org/grails/orm/hibernate/cfg/Table.groovy | 85 + .../HibernateEntityTransformation.groovy | 347 ++ ...tractHibernateConnectionSourceFactory.java | 144 + .../HibernateConnectionSource.java | 67 + .../HibernateConnectionSourceFactory.java | 287 ++ .../HibernateConnectionSourceSettings.groovy | 341 ++ ...nateConnectionSourceSettingsBuilder.groovy | 72 + .../datasource/MultipleDataSourceSupport.java | 51 + .../GrailsEntityDirtinessStrategy.groovy | 157 + .../AbstractHibernateEventListener.java | 70 + .../listener/HibernateEventListener.java | 249 ++ ...NotDetermineHibernateDialectException.java | 38 + ...GrailsHibernateConfigurationException.java | 39 + .../exceptions/GrailsHibernateException.java | 39 + .../exceptions/GrailsQueryException.java | 40 + .../MultiTenantEventListener.java | 116 + .../proxy/HibernateProxyHandler.java | 169 + .../proxy/SimpleHibernateProxyHandler.java | 173 + .../AbstractHibernateCriteriaBuilder.java | 2195 ++++++++++ .../AbstractHibernateCriterionAdapter.java | 570 +++ .../query/AbstractHibernateQuery.java | 1322 ++++++ .../query/GrailsHibernateQueryUtils.java | 431 ++ .../query/HibernateCriterionAdapter.java | 50 + .../hibernate/query/HibernateHqlQuery.java | 72 + .../query/HibernateProjectionAdapter.java | 89 + .../orm/hibernate/query/HibernateQuery.java | 130 + .../query/HibernateQueryConstants.java | 47 + .../orm/hibernate/query/PagedResultList.java | 79 + ...ractClosureEventTriggeringInterceptor.java | 50 + .../support/ClosureEventListener.java | 390 ++ .../ClosureEventTriggeringInterceptor.java | 284 ++ .../support/DataSourceFactoryBean.groovy | 58 + ...DatastoreConnectionSourcesRegistrar.groovy | 120 + .../HibernateDatastoreFactoryBean.groovy | 84 + .../HibernateDialectDetectorFactoryBean.java | 172 + .../support/HibernateRuntimeUtils.groovy | 173 + .../support/HibernateVersionSupport.java | 89 + .../grails/orm/hibernate/support/SoftKey.java | 69 + ...HibernateJtaTransactionManagerAdapter.java | 235 ++ .../PlatformTransactionManagerProxy.java | 59 + .../org.hibernate.integrator.spi.Integrator | 1 + .../spring-configuration-metadata.json | 28 + .../HibernateMappingBuilderTests.groovy | 902 +++++ ...teOptimisticLockingStyleMappingSpec.groovy | 67 + .../mapping/MappingBuilderSpec.groovy | 335 ++ .../gorm/tests/AutoTimestampSpec.groovy | 105 + .../tests/BasicCollectionInQuerySpec.groovy | 167 + ...cadeToBidirectionalAsssociationSpec.groovy | 70 + .../test/groovy/grails/gorm/tests/Club.groovy | 33 + .../tests/CompositeIdWithJoinTableSpec.groovy | 84 + ...ositeIdWithManyToOneAndSequenceSpec.groovy | 79 + .../groovy/grails/gorm/tests/Contract.groovy | 31 + .../gorm/tests/CountByWithEmbeddedSpec.groovy | 61 + .../gorm/tests/DeleteAllWhereSpec.groovy | 56 + .../tests/DetachCriteriaSubquerySpec.groovy | 174 + .../tests/DetachedCriteriaJoinSpec.groovy | 213 + ...DetachedCriteriaProjectionAliasSpec.groovy | 87 + ...iteriaProjectionNullAssociationSpec.groovy | 136 + .../DetachedCriteriaProjectionSpec.groovy | 118 + .../grails/gorm/tests/DomainGetterSpec.groovy | 47 + .../grails/gorm/tests/EnumMappingSpec.groovy | 55 + .../ExecuteQueryWithinValidatorSpec.groovy | 80 + .../HibernateEntityTraitGeneratedSpec.groovy | 44 + .../HibernateOptimisticLockingSpec.groovy | 107 + .../grails/gorm/tests/HibernateSuite.groovy | 31 + .../gorm/tests/HibernateValidationSpec.groovy | 75 + .../gorm/tests/IdentityEnumTypeSpec.groovy | 110 + .../tests/ImportFromConstraintSpec.groovy | 88 + .../LastUpdateWithDynamicUpdateSpec.groovy | 134 + .../grails/gorm/tests/ManyToOneSpec.groovy | 122 + .../MultiColumnUniqueConstraintSpec.groovy | 96 + .../gorm/tests/NullableAndLengthSpec.groovy | 61 + .../groovy/grails/gorm/tests/Player.groovy | 31 + .../groovy/grails/gorm/tests/RLikeSpec.groovy | 48 + .../gorm/tests/ReadOperationSpec.groovy | 46 + ...SaveWithExistingValidationErrorSpec.groovy | 70 + .../grails/gorm/tests/SchemaNameSpec.groovy | 59 + .../grails/gorm/tests/SequenceIdSpec.groovy | 65 + .../gorm/tests/SizeConstraintSpec.groovy | 71 + .../grails/gorm/tests/SqlQuerySpec.groovy | 149 + .../SubclassMultipleListCollectionSpec.groovy | 79 + .../gorm/tests/SubqueryAliasSpec.groovy | 60 + .../TablePerSubClassAndEmbeddedSpec.groovy | 103 + .../test/groovy/grails/gorm/tests/Team.groovy | 33 + .../grails/gorm/tests/ToOneProxySpec.groovy | 51 + .../TwoBidirectionalOneToManySpec.groovy | 75 + .../UniqueConstraintHibernateSpec.groovy | 145 + .../UniqueWithMultipleDataSourcesSpec.groovy | 85 + .../gorm/tests/WhereQueryBugFixSpec.groovy | 106 + .../WhereQueryOldIssueVerificationSpec.groovy | 371 ++ .../WhereQueryWithAssociationSortSpec.groovy | 63 + ...ewSessionAndExistingTransactionSpec.groovy | 151 + .../tests/autoimport/AutoImportSpec.groovy | 43 + .../gorm/tests/autoimport/other/A.groovy | 30 + ...BidirectionalOneToOneWithUniqueSpec.groovy | 49 + .../gorm/tests/belongsto/HibernateFace.groovy | 31 + .../gorm/tests/belongsto/HibernateNose.groovy | 32 + .../compositeid/CompositeIdCriteria.groovy | 194 + ...ositeIdWithDeepOneToManyMappingSpec.groovy | 93 + ...GlobalConstraintWithCompositeIdSpec.groovy | 137 + .../HibernateDirtyCheckingSpec.groovy | 174 + .../HibernateUpdateFromListenerSpec.groovy | 97 + .../dirtychecking/PropertyFieldSpec.groovy | 55 + .../UpdatePropertyInEventListenerSpec.groovy | 114 + .../hasmany/HasManyWithInQuerySpec.groovy | 121 + .../tests/hasmany/ListCollectionSpec.groovy | 68 + .../TwoUnidirectionalHasManySpec.groovy | 133 + .../inheritance/SubclassToOneProxySpec.groovy | 54 + ...ePerConcreteClassAndDateCreatedSpec.groovy | 77 + .../TablePerConcreteClassImportedSpec.groovy | 36 + .../gorm/tests/jpa/SimpleJpaEntitySpec.groovy | 116 + .../mappedby/MultipleOneToOneSpec.groovy | 84 + ...iTenancyBidirectionalManyToManySpec.groovy | 152 + ...iTenancyUnidirectionalOneToManySpec.groovy | 129 + .../gorm/tests/perf/JoinPerfSpec.groovy | 111 + .../tests/proxy/ByteBuddyProxySpec.groovy | 150 + .../gorm/tests/proxy/StaticTestUtil.groovy | 72 + .../tests/services/DataServiceSpec.groovy | 536 +++ .../tests/softdelete/SoftDeleteSpec.groovy | 81 + .../tests/traits/InterfacePropertySpec.groovy | 59 + .../tests/traits/TraitPropertySpec.groovy | 55 + .../tests/txs/CustomIsolationLevelSpec.groovy | 53 + .../txs/TransactionPropagationSpec.groovy | 90 + .../TransactionalWithinReadOnlySpec.groovy | 63 + .../gorm/tests/uuid/UuidInsertSpec.groovy | 60 + .../validation/BeanValidationSpec.groovy | 60 + .../validation/CascadeValidationSpec.groovy | 78 + .../validation/DeepValidationSpec.groovy | 114 + ...EmbeddedWithValidationExceptionSpec.groovy | 72 + .../SaveWithInvalidEntitySpec.groovy | 62 + .../validation/SkipValidationSpec.groovy | 120 + .../UniqueFalseConstraintSpec.groovy | 62 + .../validation/UniqueInheritanceSpec.groovy | 103 + .../validation/UniqueWithHasOneSpec.groovy | 81 + .../validation/UniqueWithinGroupSpec.groovy | 104 + .../GrailsDataHibernate7TckManager.groovy | 217 + .../hibernate/DefaultConstraintsSpec.groovy | 84 + .../orm/hibernate/ExistsCrossJoinSpec.groovy | 122 + .../hibernate/HibernateDatastoreSpec.groovy | 39 + .../cfg/HibernateMappingContextSpec.groovy | 83 + .../HibernateEntityTransformationSpec.groovy | 188 + ...ataServiceDatasourceInheritanceSpec.groovy | 239 ++ .../DataServiceMultiDataSourceSpec.groovy | 460 +++ ...rviceMultiTenantMultiDataSourceSpec.groovy | 287 ++ ...taSourceConnectionSourceFactorySpec.groovy | 54 + ...ibernateConnectionSourceFactorySpec.groovy | 66 + ...bernateConnectionSourceSettingsSpec.groovy | 84 + .../MultipleDataSourceConnectionsSpec.groovy | 218 + .../MultipleDataSourceMetadataSpec.groovy | 91 + .../MultipleDataSourcesWithCachingSpec.groovy | 75 + .../MultipleDataSourcesWithEventsSpec.groovy | 130 + .../PartitionedMultiTenancySpec.groovy | 397 ++ .../connections/SchemaMultiTenantSpec.groovy | 182 + .../connections/SecondLevelCacheSpec.groovy | 97 + .../connections/SingleTenantSpec.groovy | 177 + .../WhereQueryMultiDataSourceSpec.groovy | 179 + .../SimpleHibernateProxyHandlerSpec.groovy | 65 + .../HibernateVersionSupportSpec.groovy | 33 + ...data.testing.tck.base.GrailsDataTckManager | 20 + .../test/resources/simplelogger.properties | 22 + .../dbmigration/build.gradle | 87 + .../command/DbmChangelogSyncCommand.groovy | 37 + .../command/DbmChangelogSyncSqlCommand.groovy | 41 + .../command/DbmClearChecksumsCommand.groovy | 37 + .../command/DbmDbDocCommand.groovy | 38 + .../command/DbmDiffCommand.groovy | 69 + .../command/DbmDropAllCommand.groovy | 45 + .../DbmFutureRollbackCountSqlCommand.groovy | 51 + .../DbmFutureRollbackSqlCommand.groovy | 42 + .../DbmGenerateChangelogCommand.groovy | 58 + .../DbmGenerateGormChangelogCommand.groovy | 59 + .../command/DbmGormDiffCommand.groovy | 60 + .../command/DbmListLocksCommand.groovy | 58 + .../DbmMarkNextChangesetRanCommand.groovy | 37 + .../DbmMarkNextChangesetRanSqlCommand.groovy | 41 + .../DbmPreviousChangesetSqlCommand.groovy | 63 + .../command/DbmReleaseLocksCommand.groovy | 37 + .../command/DbmRollbackCommand.groovy | 44 + .../command/DbmRollbackCountCommand.groovy | 47 + .../command/DbmRollbackCountSqlCommand.groovy | 51 + .../command/DbmRollbackSqlCommand.groovy | 48 + .../command/DbmRollbackToDateCommand.groovy | 55 + .../DbmRollbackToDateSqlCommand.groovy | 67 + .../command/DbmStatusCommand.groovy | 42 + .../command/DbmTagCommand.groovy | 43 + .../command/DbmUpdateCommand.groovy | 40 + .../command/DbmUpdateCountCommand.groovy | 47 + .../command/DbmUpdateCountSqlCommand.groovy | 50 + .../command/DbmUpdateSqlCommand.groovy | 42 + .../command/DbmValidateCommand.groovy | 37 + .../grails-app/conf/application.yml | 28 + .../grails-app/conf/logback.groovy | 56 + .../grails-app/domain/testapp/Account.groovy | 26 + .../grails-app/domain/testapp/Person.groovy | 31 + .../init/databasemigration/Application.groovy | 34 + .../AutoRunWithMultipleDataSourceSpec.groovy | 64 + .../AutoRunWithSingleDataSourceSpec.groovy | 54 + .../DbUpdateCommandSpec.groovy | 88 + .../application-multiple-datasource.yml | 44 + .../application-single-datasource.yml | 33 + .../application-transaction-datasource.yml | 44 + .../changelog-account-person-init.groovy | 76 + .../resources/changelog-account-sql.groovy | 24 + .../resources/changelog-person-grails.groovy | 41 + .../resources/changelog-second.groovy | 61 + .../resources/changelog-transaction.groovy | 24 + .../resources/changelog.groovy | 75 + .../resources/logback-test.xml | 36 + .../DatabaseMigrationException.groovy | 24 + .../DatabaseMigrationGrailsPlugin.groovy | 130 + ...DatabaseMigrationTransactionManager.groovy | 145 + .../EnvironmentAwareCodeGenConfig.groovy | 38 + .../databasemigration/NoopVisitor.groovy | 43 + .../databasemigration/PluginConstants.groovy | 27 + ...tionContextDatabaseMigrationCommand.groovy | 129 + .../command/DatabaseMigrationCommand.groovy | 429 ++ .../command/DbmChangelogToGroovy.groovy | 83 + .../command/DbmCreateChangelog.groovy | 60 + .../ScriptDatabaseMigrationCommand.groovy | 71 + .../liquibase/ChangelogXml2Groovy.groovy | 111 + .../liquibase/DatabaseChangeLogBuilder.groovy | 133 + .../liquibase/EmbeddedJarPathHandler.groovy | 97 + .../liquibase/GormDatabase.groovy | 87 + .../liquibase/GrailsLiquibase.groovy | 106 + .../liquibase/GrailsLiquibaseFactory.groovy | 43 + .../liquibase/GroovyChange.groovy | 332 ++ .../liquibase/GroovyChangeLogParser.groovy | 106 + .../GroovyChangeLogSerializer.groovy | 60 + .../GroovyDiffToChangeLogCommandStep.groovy | 82 + .../GroovyGenerateChangeLogCommandStep.groovy | 102 + .../liquibase/GroovyPrecondition.groovy | 202 + .../META-INF/services/liquibase.change.Change | 1 + .../services/liquibase.command.CommandStep | 2 + .../services/liquibase.database.Database | 1 + .../services/liquibase.parser.ChangeLogParser | 1 + .../liquibase.precondition.Precondition | 1 + .../services/liquibase.resource.PathHandler | 1 + .../liquibase.serializer.ChangeLogSerializer | 1 + .../spring-configuration-metadata.json | 106 + .../src/main/resources/migration.gdsl | 686 ++++ .../scripts/dbm-changelog-to-groovy.groovy | 36 + .../main/scripts/dbm-create-changelog.groovy | 35 + ...ContextDatabaseMigrationCommandSpec.groovy | 127 + .../DatabaseMigrationCommandConfigSpec.groovy | 112 + .../DatabaseMigrationCommandSpec.groovy | 60 + .../DbmChangelogSyncCommandSpec.groovy | 50 + .../DbmChangelogSyncCommandSqlSpec.groovy | 68 + .../DbmClearChecksumsCommandSpec.groovy | 46 + .../command/DbmDiffCommandSpec.groovy | 140 + .../command/DbmDropAllCommandSpec.groovy | 43 + ...bmFutureRollbackCountSqlCommandSpec.groovy | 135 + .../DbmFutureRollbackSqlCommandSpec.groovy | 106 + .../DbmGenerateChangelogCommandSpec.groovy | 117 + ...DbmGenerateGormChangelogCommandSpec.groovy | 155 + .../command/DbmGormDiffCommandSpec.groovy | 127 + .../command/DbmListLocksCommandSpec.groovy | 61 + .../DbmMarkNextChangesetRanCommandSpec.groovy | 51 + ...mMarkNextChangesetRanSqlCommandSpec.groovy | 68 + .../DbmPreviousChangesetSqlCommandSpec.groovy | 134 + .../command/DbmReleaseLocksCommandSpec.groovy | 41 + .../command/DbmRollbackCommandSpec.groovy | 100 + .../DbmRollbackCountCommandSpec.groovy | 107 + .../DbmRollbackCountSqlCommandSpec.groovy | 123 + .../command/DbmRollbackSqlCommandSpec.groovy | 116 + .../DbmRollbackToDateCommandSpec.groovy | 114 + .../DbmRollbackToDateSqlCommandSpec.groovy | 132 + .../command/DbmStatusCommandSpec.groovy | 64 + .../command/DbmUpdateCommandSpec.groovy | 120 + .../command/DbmUpdateCountCommandSpec.groovy | 101 + .../DbmUpdateCountSqlCommandSpec.groovy | 137 + .../command/DbmUpdateSqlCommandSpec.groovy | 140 + .../command/DbmValidateCommandSpec.groovy | 90 + .../ScriptDatabaseMigrationCommandSpec.groovy | 59 + .../liquibase/GroovyChangeLogSpec.groovy | 240 ++ .../liquibase/GroovyPreconditionSpec.groovy | 284 ++ .../testing/OutputCaptureExtension.groovy | 113 + .../testing/annotation/OutputCapture.groovy | 34 + .../src/test/resources/logback.groovy | 38 + grails-data-hibernate7/docs/build.gradle | 146 + .../docs/asciidoc/advancedGORMFeatures.adoc | 20 + .../defaultSortOrder.adoc | 67 + .../eventsAutoTimestamping.adoc | 410 ++ .../asciidoc/advancedGORMFeatures/ormdsl.adoc | 47 + .../advancedGORMFeatures/ormdsl/caching.adoc | 167 + .../ormdsl/compositePrimaryKeys.adoc | 88 + .../ormdsl/customCascadeBehaviour.adoc | 58 + .../ormdsl/customHibernateTypes.adoc | 89 + .../ormdsl/customNamingStrategy.adoc | 81 + .../ormdsl/databaseIndices.adoc | 37 + .../ormdsl/derivedProperties.adoc | 96 + .../ormdsl/fetchingDSL.adoc | 195 + .../advancedGORMFeatures/ormdsl/identity.adoc | 55 + .../ormdsl/inheritanceStrategies.adoc | 37 + .../optimisticLockingAndVersioning.adoc | 58 + .../ormdsl/tableAndColumnNames.adoc | 219 + .../configuration/configurationDefaults.adoc | 59 + .../configuration/configurationReference.adoc | 67 + .../configuration/hibernateCustomization.adoc | 48 + .../docs/asciidoc/configuration/index.adoc | 59 + .../constraints/applyingConstraints.adoc | 154 + .../constraints/constraintReference.adoc | 43 + .../asciidoc/constraints/gormConstraints.adoc | 131 + .../src/docs/asciidoc/constraints/index.adoc | 32 + .../databaseMigration/configuration.adoc | 68 + .../asciidoc/databaseMigration/dbdoc.adoc | 42 + .../databaseMigration/generalUsage.adoc | 114 + .../databaseMigration/gettingStarted.adoc | 115 + .../docs/asciidoc/databaseMigration/gorm.adoc | 44 + .../databaseMigration/groovyChanges.adoc | 110 + .../groovyPreconditions.adoc | 87 + .../asciidoc/databaseMigration/index.adoc | 153 + .../databaseMigration/introduction.adoc | 36 + .../ref/Diff Scripts/dbm-diff.adoc | 62 + .../ref/Diff Scripts/dbm-gorm-diff.adoc | 64 + .../ref/Documentation Scripts/dbm-db-doc.adoc | 55 + .../dbm-add-migration.adoc | 41 + .../dbm-changelog-sync-sql.adoc | 56 + .../dbm-changelog-sync.adoc | 55 + .../dbm-changelog-to-groovy.adoc | 42 + .../dbm-clear-checksums.adoc | 51 + .../dbm-create-changelog.adoc | 55 + .../ref/Maintenance Scripts/dbm-drop-all.adoc | 53 + .../Maintenance Scripts/dbm-list-locks.adoc | 53 + .../Maintenance Scripts/dbm-list-tags.adoc | 46 + .../dbm-mark-next-changeset-ran.adoc | 56 + .../dbm-release-locks.adoc | 52 + .../ref/Maintenance Scripts/dbm-status.adoc | 55 + .../ref/Maintenance Scripts/dbm-tag.adoc | 56 + .../ref/Maintenance Scripts/dbm-validate.adoc | 53 + .../dbm-future-rollback-sql.adoc | 54 + .../dbm-generate-changelog.adoc | 61 + .../dbm-generate-gorm-changelog.adoc | 59 + .../dbm-rollback-count-sql.adoc | 57 + .../Rollback Scripts/dbm-rollback-count.adoc | 56 + .../Rollback Scripts/dbm-rollback-sql.adoc | 58 + .../dbm-rollback-to-date-sql.adoc | 60 + .../dbm-rollback-to-date.adoc | 58 + .../ref/Rollback Scripts/dbm-rollback.adoc | 57 + .../dbm-previous-changeset-sql.adoc | 51 + .../Update Scripts/dbm-update-count-sql.adoc | 60 + .../ref/Update Scripts/dbm-update-count.adoc | 57 + .../ref/Update Scripts/dbm-update-sql.adoc | 58 + .../ref/Update Scripts/dbm-update.adoc | 55 + .../docs/src/docs/asciidoc/domainClasses.adoc | 53 + .../domainClasses/gormAssociation.adoc | 21 + .../gormAssociation/basicCollectionTypes.adoc | 57 + .../gormAssociation/manyToMany.adoc | 62 + .../gormAssociation/manyToOneAndOneToOne.adoc | 240 ++ .../gormAssociation/oneToMany.adoc | 112 + .../domainClasses/gormComposition.adoc | 40 + .../domainClasses/inheritanceInGORM.adoc | 74 + .../domainClasses/sets,ListsAndMaps.adoc | 186 + .../src/docs/asciidoc/gettingStarted.adoc | 71 + .../gettingStarted/hibernateVersions.adoc | 89 + .../gettingStarted/outsideGrails.adoc | 72 + .../asciidoc/gettingStarted/springBoot.adoc | 111 + .../asciidoc/images/5.2.2-composition.jpg | Bin 0 -> 23811 bytes .../src/docs/asciidoc/images/GORM-1to1.png | Bin 0 -> 17532 bytes .../docs/src/docs/asciidoc/images/console.png | Bin 0 -> 22067 bytes .../src/docs/asciidoc/images/doc-template.png | Bin 0 -> 103631 bytes .../src/docs/asciidoc/images/errors-view.png | Bin 0 -> 207906 bytes .../docs/src/docs/asciidoc/images/favicon.ico | Bin 0 -> 10134 bytes .../docs/src/docs/asciidoc/images/g2one.png | Bin 0 -> 24260 bytes .../src/docs/asciidoc/images/grails-icon.png | Bin 0 -> 3261 bytes .../docs/src/docs/asciidoc/images/grails.png | Bin 0 -> 21146 bytes .../docs/src/docs/asciidoc/images/groovy.png | Bin 0 -> 7543 bytes .../src/docs/asciidoc/images/h2-console.png | Bin 0 -> 42596 bytes .../images/interactive-complete-class.png | Bin 0 -> 153909 bytes .../images/interactive-helloworld.png | Bin 0 -> 16621 bytes .../asciidoc/images/interactive-open-cmd.png | Bin 0 -> 91319 bytes .../asciidoc/images/interactive-output.png | Bin 0 -> 77641 bytes .../images/interactive-run-external.png | Bin 0 -> 63097 bytes .../src/docs/asciidoc/images/intropage.png | Bin 0 -> 110038 bytes .../docs/src/docs/asciidoc/images/logging.png | Bin 0 -> 22671 bytes .../docs/src/docs/asciidoc/images/note.gif | Bin 0 -> 569 bytes .../docs/asciidoc/images/scaffolding-ui.png | Bin 0 -> 33626 bytes .../src/docs/asciidoc/images/test-output.png | Bin 0 -> 119636 bytes .../docs/asciidoc/images/test-template.png | Bin 0 -> 65896 bytes .../src/docs/asciidoc/images/war-output.png | Bin 0 -> 51915 bytes .../docs/src/docs/asciidoc/images/warning.gif | Bin 0 -> 613 bytes .../docs/src/docs/asciidoc/index.adoc | 299 ++ .../docs/src/docs/asciidoc/introduction.adoc | 31 + .../asciidoc/introduction/releaseHistory.adoc | 96 + .../asciidoc/introduction/upgradeNotes.adoc | 50 + .../docs/src/docs/asciidoc/learningMore.adoc | 20 + .../multiTenancy/databasePerTenant.adoc | 132 + .../discriminatorMultiTenancy.adoc | 100 + .../src/docs/asciidoc/multiTenancy/index.adoc | 46 + .../src/docs/asciidoc/multiTenancy/modes.adoc | 26 + .../multiTenancy/schemaPerTenant.adoc | 72 + .../multiTenancy/tenantResolvers.adoc | 104 + .../multiTenancy/tenantTransforms.adoc | 57 + .../multipleDataSources/configuration.adoc | 43 + .../dataSourceNamespaces.adoc | 65 + .../asciidoc/multipleDataSources/index.adoc | 55 + .../mappingDomainsToDataSources.adoc | 79 + .../src/docs/asciidoc/persistenceBasics.adoc | 52 + .../asciidoc/persistenceBasics/cascades.adoc | 233 ++ .../persistenceBasics/deletingObjects.adoc | 68 + .../asciidoc/persistenceBasics/fetching.adoc | 136 + .../asciidoc/persistenceBasics/locking.adoc | 105 + .../modificationChecking.adoc | 125 + .../persistenceBasics/savingAndUpdating.adoc | 62 + .../asciidoc/programmaticTransactions.adoc | 79 + .../docs/src/docs/asciidoc/querying.adoc | 74 + .../src/docs/asciidoc/querying/criteria.adoc | 474 +++ .../asciidoc/querying/detachedCriteria.adoc | 213 + .../src/docs/asciidoc/querying/finders.adoc | 148 + .../docs/src/docs/asciidoc/querying/hql.adoc | 79 + .../docs/asciidoc/querying/whereQueries.adoc | 471 +++ .../src/docs/asciidoc/quickStartGuide.adoc | 92 + .../asciidoc/quickStartGuide/basicCRUD.adoc | 92 + .../src/docs/asciidoc/services/basics.adoc | 121 + .../docs/asciidoc/services/finderQueries.adoc | 36 + .../docs/asciidoc/services/hqlQueries.adoc | 55 + .../src/docs/asciidoc/services/index.adoc | 49 + .../services/multipleDataSources.adoc | 262 ++ .../asciidoc/services/projectionQueries.adoc | 19 + .../src/docs/asciidoc/services/queries.adoc | 69 + .../asciidoc/services/queryConventions.adoc | 53 + .../asciidoc/services/queryProjections.adoc | 91 + .../docs/asciidoc/services/rxServices.adoc | 75 + .../asciidoc/services/serviceValidation.adoc | 35 + .../docs/asciidoc/services/simpleQueries.adoc | 76 + .../docs/asciidoc/services/whereQueries.adoc | 33 + .../asciidoc/services/writeOperations.adoc | 98 + .../docs/src/docs/asciidoc/testing/index.adoc | 31 + .../docs/src/docs/asciidoc/testing/junit.adoc | 60 + .../docs/src/docs/asciidoc/testing/spock.adoc | 128 + .../docs/src/docs/resources/index.html | 10 + .../grails-plugin/build.gradle | 90 + ...HibernateDatastoreSpringInitializer.groovy | 208 + .../hibernate/HibernateGrailsPlugin.groovy | 106 + .../commands/SchemaExportCommand.groovy | 103 + .../test/hibernate/HibernateSpec.groovy | 166 + ...ggregatePersistenceContextInterceptor.java | 119 + ...ggregatePersistenceContextInterceptor.java | 44 + .../GrailsOpenSessionInViewInterceptor.java | 222 + ...ibernatePersistenceContextInterceptor.java | 262 ++ ...oryAwarePersistenceContextInterceptor.java | 34 + ...rnateDatastoreSpringInitializerSpec.groovy | 131 + .../HibernateSpecOverrideSpec.groovy | 34 + .../mixin/hibernate/HibernateSpecSpec.groovy | 100 + .../support/MultiDataSourceSessionSpec.groovy | 193 + .../src/test/resources/application.yml | 21 + .../spring-orm/build.gradle | 55 + .../hibernate5/ConfigurableJtaPlatform.java | 109 + .../support/hibernate5/HibernateCallback.java | 55 + .../HibernateExceptionTranslator.java | 102 + .../hibernate5/HibernateJdbcException.java | 59 + ...ernateObjectRetrievalFailureException.java | 57 + .../hibernate5/HibernateOperations.java | 851 ++++ ...nateOptimisticLockingFailureException.java | 49 + .../hibernate5/HibernateQueryException.java | 48 + .../hibernate5/HibernateSystemException.java | 45 + .../support/hibernate5/HibernateTemplate.java | 1157 ++++++ .../HibernateTransactionManager.java | 901 ++++ .../hibernate5/LocalSessionFactoryBean.java | 660 +++ .../LocalSessionFactoryBuilder.java | 452 +++ .../hibernate5/SessionFactoryUtils.java | 258 ++ .../support/hibernate5/SessionHolder.java | 81 + .../hibernate5/SpringBeanContainer.java | 254 ++ .../SpringFlushSynchronization.java | 54 + .../hibernate5/SpringJtaSessionContext.java | 49 + .../hibernate5/SpringSessionContext.java | 138 + .../SpringSessionSynchronization.java | 143 + .../support/AsyncRequestInterceptor.java | 122 + .../support/OpenSessionInViewInterceptor.java | 210 + .../src/main/resources/META-INF/NOTICE | 15 + .../build.gradle | 50 + .../grails-app/conf/application.yml | 50 + .../grails-app/conf/logback.xml | 37 + .../grails-app/domain/example/Product.groovy | 41 + .../init/example/Application.groovy | 31 + .../example/InheritedProductService.groovy | 38 + .../services/example/ProductService.groovy | 58 + ...ataServiceDatasourceInheritanceSpec.groovy | 112 + .../DataServiceMultiDataSourceSpec.groovy | 180 + .../grails-data-service/build.gradle | 55 + .../grails-app/conf/application.yml | 68 + .../grails-app/conf/logback.xml | 37 + .../grails-app/conf/spring/resources.groovy | 31 + .../example/ApplicationController.groovy | 33 + .../controllers/example/UrlMappings.groovy | 36 + .../grails-app/domain/example/Book.groovy | 24 + .../grails-app/domain/example/Person.groovy | 26 + .../grails-app/domain/example/Student.groovy | 26 + .../grails-app/i18n/messages.properties | 71 + .../init/example/Application.groovy | 32 + .../grails-app/init/example/BootStrap.groovy | 29 + .../services/example/BookService.groovy | 28 + .../services/example/LibraryService.groovy | 41 + .../services/example/PersonService.groovy | 29 + .../services/example/StudentService.groovy | 46 + .../services/example/TestService.groovy | 33 + .../grails-app/views/application/index.gson | 52 + .../grails-app/views/error.gson | 25 + .../grails-app/views/errors/_errors.gson | 61 + .../grails-app/views/notFound.gson | 25 + .../grails-app/views/object/_object.gson | 24 + .../example/ServiceInjectionSpec.groovy | 40 + .../groovy/example/StudentServiceSpec.groovy | 44 + .../groovy/example/TestServiceSpec.groovy | 65 + .../groovy/example/ClassUsingAService.groovy | 33 + .../src/main/groovy/example/TestBean.groovy | 39 + .../grails-database-per-tenant/build.gradle | 68 + .../assets/images/apple-touch-icon-retina.png | Bin 0 -> 14986 bytes .../assets/images/apple-touch-icon.png | Bin 0 -> 5434 bytes .../grails-app/assets/images/favicon.ico | Bin 0 -> 10134 bytes .../grails-app/assets/images/grails_logo.png | Bin 0 -> 10172 bytes .../assets/images/skin/database_add.png | Bin 0 -> 658 bytes .../assets/images/skin/database_delete.png | Bin 0 -> 659 bytes .../assets/images/skin/database_edit.png | Bin 0 -> 767 bytes .../assets/images/skin/database_save.png | Bin 0 -> 755 bytes .../assets/images/skin/database_table.png | Bin 0 -> 726 bytes .../assets/images/skin/exclamation.png | Bin 0 -> 701 bytes .../grails-app/assets/images/skin/house.png | Bin 0 -> 806 bytes .../assets/images/skin/information.png | Bin 0 -> 778 bytes .../grails-app/assets/images/skin/shadow.jpg | Bin 0 -> 300 bytes .../assets/images/skin/sorted_asc.gif | Bin 0 -> 835 bytes .../assets/images/skin/sorted_desc.gif | Bin 0 -> 834 bytes .../grails-app/assets/images/spinner.gif | Bin 0 -> 2037 bytes .../assets/javascripts/application.js | 39 + .../assets/stylesheets/application.css | 32 + .../grails-app/assets/stylesheets/errors.css | 128 + .../grails-app/assets/stylesheets/main.css | 588 +++ .../grails-app/assets/stylesheets/mobile.css | 101 + .../grails-app/conf/application.yml | 74 + .../grails-app/conf/logback.xml | 37 + .../controllers/example/BookController.groovy | 127 + .../controllers/example/UrlMappings.groovy | 44 + .../grails-app/domain/example/Book.groovy | 32 + .../grails-app/i18n/messages.properties | 70 + .../grails-app/i18n/messages_cs_CZ.properties | 70 + .../grails-app/i18n/messages_da.properties | 71 + .../grails-app/i18n/messages_de.properties | 70 + .../grails-app/i18n/messages_es.properties | 70 + .../grails-app/i18n/messages_fr.properties | 34 + .../grails-app/i18n/messages_it.properties | 70 + .../grails-app/i18n/messages_ja.properties | 70 + .../grails-app/i18n/messages_nb.properties | 71 + .../grails-app/i18n/messages_nl.properties | 70 + .../grails-app/i18n/messages_pl.properties | 74 + .../grails-app/i18n/messages_pt_BR.properties | 74 + .../grails-app/i18n/messages_pt_PT.properties | 49 + .../grails-app/i18n/messages_ru.properties | 46 + .../grails-app/i18n/messages_sv.properties | 70 + .../grails-app/i18n/messages_th.properties | 70 + .../grails-app/i18n/messages_zh_CN.properties | 33 + .../init/datasources/Application.groovy | 31 + .../example/AnotherBookService.groovy | 41 + .../services/example/BookService.groovy | 43 + .../grails-app/views/book/create.gsp | 56 + .../grails-app/views/book/edit.gsp | 58 + .../grails-app/views/book/index.gsp | 46 + .../grails-app/views/book/show.gsp | 49 + .../grails-app/views/error.gsp | 49 + .../grails-app/views/index.gsp | 147 + .../grails-app/views/layouts/main.gsp | 37 + .../grails-app/views/notFound.gsp | 32 + .../DatabasePerTenantIntegrationSpec.groovy | 118 + .../example/DatabasePerTenantSpec.groovy | 96 + .../build.gradle | 50 + .../grails-app/conf/application.yml | 32 + .../grails-app/conf/logback.xml | 37 + .../grails-app/domain/example/Customer.groovy | 46 + .../init/datasources/Application.groovy | 31 + .../src/test/groovy/example/ProxySpec.groovy | 59 + .../hibernate7/grails-hibernate/build.gradle | 79 + .../assets/images/apple-touch-icon-retina.png | Bin 0 -> 14986 bytes .../assets/images/apple-touch-icon.png | Bin 0 -> 5434 bytes .../grails-app/assets/images/favicon.ico | Bin 0 -> 10134 bytes .../grails-app/assets/images/grails_logo.png | Bin 0 -> 10172 bytes .../assets/images/skin/database_add.png | Bin 0 -> 658 bytes .../assets/images/skin/database_delete.png | Bin 0 -> 659 bytes .../assets/images/skin/database_edit.png | Bin 0 -> 767 bytes .../assets/images/skin/database_save.png | Bin 0 -> 755 bytes .../assets/images/skin/database_table.png | Bin 0 -> 726 bytes .../assets/images/skin/exclamation.png | Bin 0 -> 701 bytes .../grails-app/assets/images/skin/house.png | Bin 0 -> 806 bytes .../assets/images/skin/information.png | Bin 0 -> 778 bytes .../grails-app/assets/images/skin/shadow.jpg | Bin 0 -> 300 bytes .../assets/images/skin/sorted_asc.gif | Bin 0 -> 835 bytes .../assets/images/skin/sorted_desc.gif | Bin 0 -> 834 bytes .../grails-app/assets/images/spinner.gif | Bin 0 -> 2037 bytes .../assets/javascripts/application.js | 39 + .../assets/stylesheets/application.css | 32 + .../grails-app/assets/stylesheets/errors.css | 128 + .../grails-app/assets/stylesheets/main.css | 588 +++ .../grails-app/assets/stylesheets/mobile.css | 101 + .../grails-app/conf/application.yml | 84 + .../grails-app/conf/logback.xml | 37 + .../grails-app/conf/spring/resources.groovy | 22 + .../functional/tests/BookController.groovy | 133 + .../functional/tests/ProductController.groovy | 34 + .../functional/tests/UrlMappings.groovy | 35 + .../domain/functional/tests/Book.groovy | 29 + .../domain/functional/tests/Business.groovy | 30 + .../domain/functional/tests/Employee.groovy | 28 + .../domain/functional/tests/Person.groovy | 24 + .../domain/functional/tests/Product.groovy | 41 + .../grails-app/i18n/messages.properties | 70 + .../grails-app/i18n/messages_cs_CZ.properties | 70 + .../grails-app/i18n/messages_da.properties | 71 + .../grails-app/i18n/messages_de.properties | 70 + .../grails-app/i18n/messages_es.properties | 70 + .../grails-app/i18n/messages_fr.properties | 34 + .../grails-app/i18n/messages_it.properties | 70 + .../grails-app/i18n/messages_ja.properties | 70 + .../grails-app/i18n/messages_nb.properties | 71 + .../grails-app/i18n/messages_nl.properties | 70 + .../grails-app/i18n/messages_pl.properties | 74 + .../grails-app/i18n/messages_pt_BR.properties | 74 + .../grails-app/i18n/messages_pt_PT.properties | 49 + .../grails-app/i18n/messages_ru.properties | 46 + .../grails-app/i18n/messages_sv.properties | 70 + .../grails-app/i18n/messages_th.properties | 70 + .../grails-app/i18n/messages_zh_CN.properties | 33 + .../init/functional/tests/Application.groovy | 36 + .../init/functional/tests/BootStrap.groovy | 37 + .../functional/tests/BookService.groovy | 31 + .../grails-app/views/book/create.gsp | 56 + .../grails-app/views/book/edit.gsp | 58 + .../grails-app/views/book/index.gsp | 46 + .../grails-app/views/book/show.gsp | 49 + .../grails-app/views/error.gsp | 49 + .../grails-app/views/index.gsp | 141 + .../grails-app/views/layouts/main.gsp | 37 + .../grails-app/views/notFound.gsp | 32 + .../tests/BookControllerSpec.groovy | 42 + .../tests/CascadeValidationSpec.groovy | 47 + .../functional/tests/ProductSpec.groovy | 68 + .../functional/tests/pages/BookPages.groovy | 58 + .../src/main/groovy/another/Item.groovy | 40 + ...ibernateMappingContextConfiguration.groovy | 28 + .../tests/BookControllerUnitSpec.groovy | 182 + .../grails-multiple-datasources/build.gradle | 73 + .../grails-app/conf/application.yml | 69 + .../grails-app/conf/logback.xml | 37 + .../SecondaryBookController.groovy | 78 + .../datasources/UrlMappings.groovy | 34 + .../grails-app/domain/ds2/Book.groovy | 34 + .../grails-app/domain/example/Book.groovy | 30 + .../init/datasources/Application.groovy | 31 + .../services/example/BookService.groovy | 35 + .../MultiDataSourceWithSessionSpec.groovy | 76 + .../MultipleDataSourcesSpec.groovy | 54 + .../build.gradle | 50 + .../grails-app/conf/application.yml | 54 + .../grails-app/conf/logback.xml | 37 + .../grails-app/domain/example/Metric.groovy | 45 + .../init/example/Application.groovy | 30 + .../services/example/MetricService.groovy | 65 + .../MultiTenantMultiDataSourceSpec.groovy | 183 + .../build.gradle | 67 + .../assets/images/apple-touch-icon-retina.png | Bin 0 -> 14986 bytes .../assets/images/apple-touch-icon.png | Bin 0 -> 5434 bytes .../grails-app/assets/images/favicon.ico | Bin 0 -> 10134 bytes .../grails-app/assets/images/grails_logo.png | Bin 0 -> 10172 bytes .../assets/images/skin/database_add.png | Bin 0 -> 658 bytes .../assets/images/skin/database_delete.png | Bin 0 -> 659 bytes .../assets/images/skin/database_edit.png | Bin 0 -> 767 bytes .../assets/images/skin/database_save.png | Bin 0 -> 755 bytes .../assets/images/skin/database_table.png | Bin 0 -> 726 bytes .../assets/images/skin/exclamation.png | Bin 0 -> 701 bytes .../grails-app/assets/images/skin/house.png | Bin 0 -> 806 bytes .../assets/images/skin/information.png | Bin 0 -> 778 bytes .../grails-app/assets/images/skin/shadow.jpg | Bin 0 -> 300 bytes .../assets/images/skin/sorted_asc.gif | Bin 0 -> 835 bytes .../assets/images/skin/sorted_desc.gif | Bin 0 -> 834 bytes .../grails-app/assets/images/spinner.gif | Bin 0 -> 2037 bytes .../assets/javascripts/application.js | 39 + .../assets/stylesheets/application.css | 32 + .../grails-app/assets/stylesheets/errors.css | 128 + .../grails-app/assets/stylesheets/main.css | 588 +++ .../grails-app/assets/stylesheets/mobile.css | 101 + .../grails-app/conf/application.yml | 68 + .../grails-app/conf/logback.xml | 37 + .../controllers/example/BookController.groovy | 127 + .../controllers/example/UrlMappings.groovy | 44 + .../grails-app/domain/example/Book.groovy | 33 + .../grails-app/i18n/messages.properties | 70 + .../grails-app/i18n/messages_cs_CZ.properties | 70 + .../grails-app/i18n/messages_da.properties | 71 + .../grails-app/i18n/messages_de.properties | 70 + .../grails-app/i18n/messages_es.properties | 70 + .../grails-app/i18n/messages_fr.properties | 34 + .../grails-app/i18n/messages_it.properties | 70 + .../grails-app/i18n/messages_ja.properties | 70 + .../grails-app/i18n/messages_nb.properties | 71 + .../grails-app/i18n/messages_nl.properties | 70 + .../grails-app/i18n/messages_pl.properties | 74 + .../grails-app/i18n/messages_pt_BR.properties | 74 + .../grails-app/i18n/messages_pt_PT.properties | 49 + .../grails-app/i18n/messages_ru.properties | 46 + .../grails-app/i18n/messages_sv.properties | 70 + .../grails-app/i18n/messages_th.properties | 70 + .../grails-app/i18n/messages_zh_CN.properties | 33 + .../init/datasources/Application.groovy | 29 + .../example/AnotherBookService.groovy | 40 + .../services/example/BookService.groovy | 43 + .../grails-app/views/book/create.gsp | 56 + .../grails-app/views/book/edit.gsp | 58 + .../grails-app/views/book/index.gsp | 46 + .../grails-app/views/book/show.gsp | 49 + .../grails-app/views/error.gsp | 49 + .../grails-app/views/index.gsp | 147 + .../grails-app/views/layouts/main.gsp | 37 + .../grails-app/views/notFound.gsp | 32 + ...titionedMultiTenancyIntegrationSpec.groovy | 118 + .../PartitionedMultiTenancySpec.groovy | 94 + .../grails-schema-per-tenant/build.gradle | 67 + .../assets/images/apple-touch-icon-retina.png | Bin 0 -> 14986 bytes .../assets/images/apple-touch-icon.png | Bin 0 -> 5434 bytes .../grails-app/assets/images/favicon.ico | Bin 0 -> 10134 bytes .../grails-app/assets/images/grails_logo.png | Bin 0 -> 10172 bytes .../assets/images/skin/database_add.png | Bin 0 -> 658 bytes .../assets/images/skin/database_delete.png | Bin 0 -> 659 bytes .../assets/images/skin/database_edit.png | Bin 0 -> 767 bytes .../assets/images/skin/database_save.png | Bin 0 -> 755 bytes .../assets/images/skin/database_table.png | Bin 0 -> 726 bytes .../assets/images/skin/exclamation.png | Bin 0 -> 701 bytes .../grails-app/assets/images/skin/house.png | Bin 0 -> 806 bytes .../assets/images/skin/information.png | Bin 0 -> 778 bytes .../grails-app/assets/images/skin/shadow.jpg | Bin 0 -> 300 bytes .../assets/images/skin/sorted_asc.gif | Bin 0 -> 835 bytes .../assets/images/skin/sorted_desc.gif | Bin 0 -> 834 bytes .../grails-app/assets/images/spinner.gif | Bin 0 -> 2037 bytes .../assets/javascripts/application.js | 39 + .../assets/stylesheets/application.css | 32 + .../grails-app/assets/stylesheets/errors.css | 128 + .../grails-app/assets/stylesheets/main.css | 588 +++ .../grails-app/assets/stylesheets/mobile.css | 101 + .../grails-app/conf/application.yml | 68 + .../grails-app/conf/logback.xml | 37 + .../schemapertenant/BookController.groovy | 127 + .../schemapertenant/UrlMappings.groovy | 44 + .../domain/schemapertenant/Book.groovy | 32 + .../grails-app/i18n/messages.properties | 70 + .../grails-app/i18n/messages_cs_CZ.properties | 70 + .../grails-app/i18n/messages_da.properties | 71 + .../grails-app/i18n/messages_de.properties | 70 + .../grails-app/i18n/messages_es.properties | 70 + .../grails-app/i18n/messages_fr.properties | 34 + .../grails-app/i18n/messages_it.properties | 70 + .../grails-app/i18n/messages_ja.properties | 70 + .../grails-app/i18n/messages_nb.properties | 71 + .../grails-app/i18n/messages_nl.properties | 70 + .../grails-app/i18n/messages_pl.properties | 74 + .../grails-app/i18n/messages_pt_BR.properties | 74 + .../grails-app/i18n/messages_pt_PT.properties | 49 + .../grails-app/i18n/messages_ru.properties | 46 + .../grails-app/i18n/messages_sv.properties | 70 + .../grails-app/i18n/messages_th.properties | 70 + .../grails-app/i18n/messages_zh_CN.properties | 33 + .../init/schemapertenant/Application.groovy | 31 + .../schemapertenant/AnotherBookService.groovy | 40 + .../schemapertenant/BookService.groovy | 43 + .../grails-app/views/book/create.gsp | 56 + .../grails-app/views/book/edit.gsp | 58 + .../grails-app/views/book/index.gsp | 46 + .../grails-app/views/book/show.gsp | 49 + .../grails-app/views/error.gsp | 49 + .../grails-app/views/index.gsp | 147 + .../grails-app/views/layouts/main.gsp | 37 + .../grails-app/views/notFound.gsp | 32 + .../SchemaPerTenantIntegrationSpec.groovy | 121 + .../SchemaPerTenantSpec.groovy | 105 + .../hibernate7/issue450/build.gradle | 71 + .../assets/images/advancedgrails.svg | 27 + .../assets/images/apple-touch-icon-retina.png | Bin 0 -> 7038 bytes .../assets/images/apple-touch-icon.png | Bin 0 -> 3077 bytes .../assets/images/documentation.svg | 19 + .../grails-app/assets/images/favicon.ico | Bin 0 -> 5558 bytes .../images/grails-cupsonly-logo-white.svg | 26 + .../grails-app/assets/images/grails.svg | 13 + .../assets/images/skin/database_add.png | Bin 0 -> 658 bytes .../assets/images/skin/database_delete.png | Bin 0 -> 659 bytes .../assets/images/skin/database_edit.png | Bin 0 -> 767 bytes .../assets/images/skin/database_save.png | Bin 0 -> 755 bytes .../assets/images/skin/database_table.png | Bin 0 -> 726 bytes .../assets/images/skin/exclamation.png | Bin 0 -> 701 bytes .../grails-app/assets/images/skin/house.png | Bin 0 -> 806 bytes .../assets/images/skin/information.png | Bin 0 -> 778 bytes .../grails-app/assets/images/skin/shadow.jpg | Bin 0 -> 300 bytes .../assets/images/skin/sorted_asc.gif | Bin 0 -> 835 bytes .../assets/images/skin/sorted_desc.gif | Bin 0 -> 834 bytes .../grails-app/assets/images/slack.svg | 18 + .../grails-app/assets/images/spinner.gif | Bin 0 -> 2037 bytes .../assets/javascripts/application.js | 29 + .../assets/stylesheets/application.css | 34 + .../grails-app/assets/stylesheets/errors.css | 128 + .../grails-app/assets/stylesheets/grails.css | 1097 +++++ .../grails-app/assets/stylesheets/main.css | 613 +++ .../grails-app/assets/stylesheets/mobile.css | 101 + .../issue450/grails-app/conf/application.yml | 84 + .../issue450/grails-app/conf/logback.xml | 37 + .../BookController.groovy | 46 + .../multitenantcomposite/UrlMappings.groovy | 35 + .../domain/multitenantcomposite/Book.groovy | 33 + .../multitenantcomposite/Application.groovy | 32 + .../multitenantcomposite/BootStrap.groovy | 43 + .../multitenantcomposite/BookService.groovy | 30 + .../issue450/grails-app/views/book/books.gsp | 30 + .../issue450/grails-app/views/book/index.gsp | 31 + .../issue450/grails-app/views/error.gsp | 49 + .../issue450/grails-app/views/index.gsp | 95 + .../grails-app/views/layouts/main.gsp | 88 + .../issue450/grails-app/views/notFound.gsp | 32 + .../groovy/example/BookControllerSpec.groovy | 39 + .../spring-boot-hibernate/build.gradle | 56 + .../main/groovy/example/Application.groovy | 44 + .../src/main/groovy/example/Book.groovy | 27 + .../main/groovy/example/BookController.groovy | 46 + .../main/groovy/example/BookService.groovy | 27 + .../src/main/resources/application.yml | 17 + .../src/test/groovy/example/BookSpec.groovy | 42 + .../standalone-hibernate/build.gradle | 50 + .../hibernate/example/ExampleSpec.groovy | 61 + .../test/resources/simplelogger.properties | 23 + settings.gradle | 61 + 884 files changed, 85774 insertions(+) create mode 100644 gradle/hibernate7-test-config.gradle create mode 100644 grails-bom/hibernate7/build.gradle create mode 100644 grails-data-hibernate7/boot-plugin/build.gradle create mode 100644 grails-data-hibernate7/boot-plugin/src/main/groovy/org/grails/datastore/gorm/boot/autoconfigure/HibernateGormAutoConfiguration.groovy create mode 100644 grails-data-hibernate7/boot-plugin/src/main/groovy/org/grails/datastore/gorm/boot/compiler/GormCompilerAutoConfiguration.groovy create mode 100644 grails-data-hibernate7/boot-plugin/src/main/resources/META-INF/services/org.grails.cli.compiler.CompilerAutoConfiguration create mode 100644 grails-data-hibernate7/boot-plugin/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 grails-data-hibernate7/boot-plugin/src/test/groovy/org/grails/datastore/gorm/boot/autoconfigure/HibernateGormAutoConfigurationSpec.groovy create mode 100644 grails-data-hibernate7/boot-plugin/src/test/groovy/org/springframework/bean/reader/GroovyBeanDefinitionReaderSpec.groovy create mode 100644 grails-data-hibernate7/core/build.gradle create mode 100644 grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/grails/orm/PagedResultList.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/grails/orm/RlikeExpression.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/HibernateEntity.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/annotation/ManagedEntity.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/mapping/MappingBuilder.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateDatastore.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormInstanceApi.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormStaticApi.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormValidationApi.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateSession.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/EventListenerIntegrator.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTransactionManager.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsSessionContext.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateEventListeners.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateMappingContextSessionFactoryBean.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/IHibernateTemplate.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/InstanceApiHelper.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/MetadataIntegrator.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SessionFactoryHolder.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/access/TraitPropertyAccessStrategy.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/AbstractGrailsDomainBinder.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/CacheConfig.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/CompositeIdentity.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/DiscriminatorConfig.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsIdentifierGeneratorFactory.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingBuilder.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernatePersistentEntity.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Identity.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/IdentityEnumType.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/InstanceProxy.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/JoinTable.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Mapping.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/NaturalId.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PersistentEntityNamingStrategy.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyConfig.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Settings.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/SortConfig.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Table.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/compiler/HibernateEntityTransformation.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/AbstractHibernateConnectionSourceFactory.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSource.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactory.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettings.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettingsBuilder.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/datasource/MultipleDataSourceSupport.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/dirty/GrailsEntityDirtinessStrategy.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/AbstractHibernateEventListener.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListener.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/CouldNotDetermineHibernateDialectException.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsHibernateConfigurationException.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsHibernateException.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsQueryException.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandler.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriterionAdapter.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateCriterionAdapter.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateProjectionAdapter.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQueryConstants.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/AbstractClosureEventTriggeringInterceptor.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/DataSourceFactoryBean.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDatastoreConnectionSourcesRegistrar.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDatastoreFactoryBean.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDialectDetectorFactoryBean.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateVersionSupport.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/SoftKey.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/transaction/HibernateJtaTransactionManagerAdapter.java create mode 100644 grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/transaction/PlatformTransactionManagerProxy.java create mode 100644 grails-data-hibernate7/core/src/main/resources/META-INF/org.hibernate.integrator.spi.Integrator create mode 100644 grails-data-hibernate7/core/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateOptimisticLockingStyleMappingSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/MappingBuilderSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/AutoTimestampSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/BasicCollectionInQuerySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CascadeToBidirectionalAsssociationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Club.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithJoinTableSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Contract.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CountByWithEmbeddedSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DeleteAllWhereSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachCriteriaSubquerySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaJoinSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionAliasSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionNullAssociationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DomainGetterSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/EnumMappingSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ExecuteQueryWithinValidatorSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateEntityTraitGeneratedSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateOptimisticLockingSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateSuite.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateValidationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ImportFromConstraintSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/LastUpdateWithDynamicUpdateSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ManyToOneSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/MultiColumnUniqueConstraintSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/NullableAndLengthSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Player.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/RLikeSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ReadOperationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SaveWithExistingValidationErrorSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SchemaNameSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SequenceIdSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SizeConstraintSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SqlQuerySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SubclassMultipleListCollectionSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SubqueryAliasSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TablePerSubClassAndEmbeddedSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Team.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ToOneProxySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TwoBidirectionalOneToManySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueConstraintHibernateSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueWithMultipleDataSourcesSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryBugFixSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryOldIssueVerificationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryWithAssociationSortSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WithNewSessionAndExistingTransactionSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/autoimport/AutoImportSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/autoimport/other/A.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/BidirectionalOneToOneWithUniqueSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/HibernateFace.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/HibernateNose.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdCriteria.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdWithDeepOneToManyMappingSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/GlobalConstraintWithCompositeIdSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateDirtyCheckingSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateUpdateFromListenerSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/PropertyFieldSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/events/UpdatePropertyInEventListenerSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/HasManyWithInQuerySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/ListCollectionSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/TwoUnidirectionalHasManySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/SubclassToOneProxySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/TablePerConcreteClassAndDateCreatedSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/TablePerConcreteClassImportedSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/jpa/SimpleJpaEntitySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/mappedby/MultipleOneToOneSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyBidirectionalManyToManySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyUnidirectionalOneToManySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/perf/JoinPerfSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/proxy/ByteBuddyProxySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/proxy/StaticTestUtil.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/services/DataServiceSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/softdelete/SoftDeleteSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/traits/InterfacePropertySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/traits/TraitPropertySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/CustomIsolationLevelSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/TransactionPropagationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/TransactionalWithinReadOnlySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/uuid/UuidInsertSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/BeanValidationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/CascadeValidationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/DeepValidationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/EmbeddedWithValidationExceptionSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/SaveWithInvalidEntitySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/SkipValidationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueFalseConstraintSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueInheritanceSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithHasOneSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithinGroupSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/DefaultConstraintsSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ExistsCrossJoinSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateDatastoreSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/compiler/HibernateEntityTransformationSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceDatasourceInheritanceSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiDataSourceSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataSourceConnectionSourceFactorySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactorySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettingsSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceMetadataSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithCachingSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithEventsSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/PartitionedMultiTenancySpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SchemaMultiTenantSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SecondLevelCacheSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SingleTenantSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandlerSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/HibernateVersionSupportSpec.groovy create mode 100644 grails-data-hibernate7/core/src/test/resources/META-INF/services/org.apache.grails.data.testing.tck.base.GrailsDataTckManager create mode 100644 grails-data-hibernate7/core/src/test/resources/simplelogger.properties create mode 100644 grails-data-hibernate7/dbmigration/build.gradle create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmChangelogSyncSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmClearChecksumsCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDbDocCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDiffCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDropAllCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmFutureRollbackCountSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmFutureRollbackSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGenerateChangelogCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGenerateGormChangelogCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGormDiffCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmListLocksCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmPreviousChangesetSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmReleaseLocksCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCountCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCountSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackToDateCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackToDateSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmStatusCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmTagCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCountCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCountSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateSqlCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmValidateCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/conf/application.yml create mode 100644 grails-data-hibernate7/dbmigration/grails-app/conf/logback.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/domain/testapp/Account.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/domain/testapp/Person.groovy create mode 100644 grails-data-hibernate7/dbmigration/grails-app/init/databasemigration/Application.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/AutoRunWithMultipleDataSourceSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/AutoRunWithSingleDataSourceSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/DbUpdateCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/application-multiple-datasource.yml create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/application-single-datasource.yml create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/application-transaction-datasource.yml create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-account-person-init.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-account-sql.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-person-grails.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-second.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-transaction.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/integration-test/resources/logback-test.xml create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationException.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationGrailsPlugin.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationTransactionManager.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/EnvironmentAwareCodeGenConfig.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/NoopVisitor.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/PluginConstants.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DbmChangelogToGroovy.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DbmCreateChangelog.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/ScriptDatabaseMigrationCommand.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/ChangelogXml2Groovy.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/DatabaseChangeLogBuilder.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/EmbeddedJarPathHandler.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabase.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibase.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibaseFactory.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChange.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogParser.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogSerializer.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyDiffToChangeLogCommandStep.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyGenerateChangeLogCommandStep.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyPrecondition.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.change.Change create mode 100644 grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.command.CommandStep create mode 100644 grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.database.Database create mode 100644 grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.parser.ChangeLogParser create mode 100644 grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.precondition.Precondition create mode 100644 grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.resource.PathHandler create mode 100644 grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.serializer.ChangeLogSerializer create mode 100644 grails-data-hibernate7/dbmigration/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 grails-data-hibernate7/dbmigration/src/main/resources/migration.gdsl create mode 100644 grails-data-hibernate7/dbmigration/src/main/scripts/dbm-changelog-to-groovy.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/main/scripts/dbm-create-changelog.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommandConfigSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommandSqlSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmClearChecksumsCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDiffCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDropAllCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmFutureRollbackCountSqlCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmFutureRollbackSqlCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGenerateChangelogCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGenerateGormChangelogCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGormDiffCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmListLocksCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanSqlCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmPreviousChangesetSqlCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmReleaseLocksCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCountCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCountSqlCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackSqlCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackToDateCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackToDateSqlCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmStatusCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCountCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCountSqlCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateSqlCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmValidateCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ScriptDatabaseMigrationCommandSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GroovyPreconditionSpec.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/testing/OutputCaptureExtension.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/testing/annotation/OutputCapture.groovy create mode 100644 grails-data-hibernate7/dbmigration/src/test/resources/logback.groovy create mode 100644 grails-data-hibernate7/docs/build.gradle create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/defaultSortOrder.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/eventsAutoTimestamping.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/caching.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/compositePrimaryKeys.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customCascadeBehaviour.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customHibernateTypes.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customNamingStrategy.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/databaseIndices.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/derivedProperties.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/fetchingDSL.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/identity.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/inheritanceStrategies.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/optimisticLockingAndVersioning.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/tableAndColumnNames.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/configuration/configurationDefaults.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/configuration/configurationReference.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/configuration/hibernateCustomization.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/configuration/index.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/constraints/applyingConstraints.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/constraints/constraintReference.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/constraints/gormConstraints.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/constraints/index.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/configuration.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/dbdoc.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/generalUsage.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/gettingStarted.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/gorm.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/groovyChanges.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/groovyPreconditions.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/index.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/introduction.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Diff Scripts/dbm-diff.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Diff Scripts/dbm-gorm-diff.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Documentation Scripts/dbm-db-doc.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-add-migration.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-sync-sql.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-sync.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-to-groovy.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-clear-checksums.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-create-changelog.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-drop-all.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-list-locks.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-list-tags.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-mark-next-changeset-ran.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-release-locks.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-status.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-tag.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-validate.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-future-rollback-sql.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-generate-changelog.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-generate-gorm-changelog.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-count-sql.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-count.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-sql.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-to-date-sql.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-to-date.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Update Scripts/dbm-previous-changeset-sql.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Update Scripts/dbm-update-count-sql.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Update Scripts/dbm-update-count.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Update Scripts/dbm-update-sql.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Update Scripts/dbm-update.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/basicCollectionTypes.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToMany.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToOneAndOneToOne.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/oneToMany.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormComposition.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/inheritanceInGORM.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/sets,ListsAndMaps.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/gettingStarted.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/gettingStarted/hibernateVersions.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/gettingStarted/outsideGrails.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/gettingStarted/springBoot.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/5.2.2-composition.jpg create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/GORM-1to1.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/console.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/doc-template.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/errors-view.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/favicon.ico create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/g2one.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/grails-icon.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/grails.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/groovy.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/h2-console.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/interactive-complete-class.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/interactive-helloworld.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/interactive-open-cmd.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/interactive-output.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/interactive-run-external.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/intropage.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/logging.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/note.gif create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/scaffolding-ui.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/test-output.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/test-template.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/war-output.png create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/images/warning.gif create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/index.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/introduction.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/introduction/releaseHistory.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/introduction/upgradeNotes.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/learningMore.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multiTenancy/databasePerTenant.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multiTenancy/discriminatorMultiTenancy.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multiTenancy/index.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multiTenancy/modes.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multiTenancy/schemaPerTenant.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multiTenancy/tenantResolvers.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multiTenancy/tenantTransforms.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multipleDataSources/configuration.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multipleDataSources/dataSourceNamespaces.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multipleDataSources/index.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/multipleDataSources/mappingDomainsToDataSources.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/cascades.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/deletingObjects.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/fetching.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/locking.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/modificationChecking.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/savingAndUpdating.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/programmaticTransactions.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/querying.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/querying/criteria.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/querying/detachedCriteria.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/querying/finders.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/querying/hql.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/querying/whereQueries.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide/basicCRUD.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/basics.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/finderQueries.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/hqlQueries.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/index.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/multipleDataSources.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/projectionQueries.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/queries.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/queryConventions.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/queryProjections.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/rxServices.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/serviceValidation.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/simpleQueries.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/whereQueries.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/services/writeOperations.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/testing/index.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/testing/junit.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/asciidoc/testing/spock.adoc create mode 100644 grails-data-hibernate7/docs/src/docs/resources/index.html create mode 100644 grails-data-hibernate7/grails-plugin/build.gradle create mode 100644 grails-data-hibernate7/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy create mode 100644 grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy create mode 100644 grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/commands/SchemaExportCommand.groovy create mode 100644 grails-data-hibernate7/grails-plugin/src/main/groovy/grails/test/hibernate/HibernateSpec.groovy create mode 100644 grails-data-hibernate7/grails-plugin/src/main/groovy/org/grails/plugin/hibernate/support/AbstractMultipleDataSourceAggregatePersistenceContextInterceptor.java create mode 100644 grails-data-hibernate7/grails-plugin/src/main/groovy/org/grails/plugin/hibernate/support/AggregatePersistenceContextInterceptor.java create mode 100644 grails-data-hibernate7/grails-plugin/src/main/groovy/org/grails/plugin/hibernate/support/GrailsOpenSessionInViewInterceptor.java create mode 100644 grails-data-hibernate7/grails-plugin/src/main/groovy/org/grails/plugin/hibernate/support/HibernatePersistenceContextInterceptor.java create mode 100644 grails-data-hibernate7/grails-plugin/src/main/groovy/org/grails/plugin/hibernate/support/SessionFactoryAwarePersistenceContextInterceptor.java create mode 100644 grails-data-hibernate7/grails-plugin/src/test/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializerSpec.groovy create mode 100644 grails-data-hibernate7/grails-plugin/src/test/groovy/grails/test/mixin/hibernate/HibernateSpecOverrideSpec.groovy create mode 100644 grails-data-hibernate7/grails-plugin/src/test/groovy/grails/test/mixin/hibernate/HibernateSpecSpec.groovy create mode 100644 grails-data-hibernate7/grails-plugin/src/test/groovy/org/grails/plugin/hibernate/support/MultiDataSourceSessionSpec.groovy create mode 100644 grails-data-hibernate7/grails-plugin/src/test/resources/application.yml create mode 100644 grails-data-hibernate7/spring-orm/build.gradle create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/ConfigurableJtaPlatform.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateCallback.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateExceptionTranslator.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateJdbcException.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateObjectRetrievalFailureException.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateOperations.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateOptimisticLockingFailureException.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateQueryException.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateSystemException.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateTemplate.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/HibernateTransactionManager.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/LocalSessionFactoryBean.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/LocalSessionFactoryBuilder.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/SessionFactoryUtils.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/SessionHolder.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/SpringBeanContainer.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/SpringFlushSynchronization.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/SpringJtaSessionContext.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/SpringSessionContext.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/SpringSessionSynchronization.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/support/AsyncRequestInterceptor.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate5/support/OpenSessionInViewInterceptor.java create mode 100644 grails-data-hibernate7/spring-orm/src/main/resources/META-INF/NOTICE create mode 100644 grails-test-examples/hibernate7/grails-data-service-multi-datasource/build.gradle create mode 100644 grails-test-examples/hibernate7/grails-data-service-multi-datasource/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/grails-data-service-multi-datasource/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/grails-data-service-multi-datasource/grails-app/domain/example/Product.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service-multi-datasource/grails-app/init/example/Application.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service-multi-datasource/grails-app/services/example/InheritedProductService.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service-multi-datasource/grails-app/services/example/ProductService.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service-multi-datasource/src/integration-test/groovy/functionaltests/DataServiceDatasourceInheritanceSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service-multi-datasource/src/integration-test/groovy/functionaltests/DataServiceMultiDataSourceSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/build.gradle create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/conf/spring/resources.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/controllers/example/ApplicationController.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/controllers/example/UrlMappings.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/domain/example/Book.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/domain/example/Person.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/domain/example/Student.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/i18n/messages.properties create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/init/example/Application.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/init/example/BootStrap.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/services/example/BookService.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/services/example/LibraryService.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/services/example/PersonService.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/services/example/StudentService.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/services/example/TestService.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/views/application/index.gson create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/views/error.gson create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/views/errors/_errors.gson create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/views/notFound.gson create mode 100644 grails-test-examples/hibernate7/grails-data-service/grails-app/views/object/_object.gson create mode 100644 grails-test-examples/hibernate7/grails-data-service/src/integration-test/groovy/example/ServiceInjectionSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/src/integration-test/groovy/example/StudentServiceSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/src/integration-test/groovy/example/TestServiceSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/src/main/groovy/example/ClassUsingAService.groovy create mode 100644 grails-test-examples/hibernate7/grails-data-service/src/main/groovy/example/TestBean.groovy create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/build.gradle create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/apple-touch-icon-retina.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/apple-touch-icon.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/favicon.ico create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/grails_logo.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/database_add.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/database_delete.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/database_edit.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/database_save.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/database_table.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/exclamation.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/house.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/information.png create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/shadow.jpg create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/sorted_asc.gif create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/skin/sorted_desc.gif create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/images/spinner.gif create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/javascripts/application.js create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/stylesheets/application.css create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/stylesheets/errors.css create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/stylesheets/main.css create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/assets/stylesheets/mobile.css create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/controllers/example/BookController.groovy create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/controllers/example/UrlMappings.groovy create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/domain/example/Book.groovy create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_cs_CZ.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_da.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_de.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_es.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_fr.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_it.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_ja.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_nb.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_nl.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_pl.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_pt_BR.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_pt_PT.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_ru.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_sv.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_th.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/i18n/messages_zh_CN.properties create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/init/datasources/Application.groovy create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/services/example/AnotherBookService.groovy create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/services/example/BookService.groovy create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/views/book/create.gsp create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/views/book/edit.gsp create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/views/book/index.gsp create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/views/book/show.gsp create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/views/error.gsp create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/views/index.gsp create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/views/layouts/main.gsp create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/views/notFound.gsp create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/src/integration-test/groovy/example/DatabasePerTenantIntegrationSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-database-per-tenant/src/test/groovy/example/DatabasePerTenantSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate-groovy-proxy/build.gradle create mode 100644 grails-test-examples/hibernate7/grails-hibernate-groovy-proxy/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/grails-hibernate-groovy-proxy/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/grails-hibernate-groovy-proxy/grails-app/domain/example/Customer.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate-groovy-proxy/grails-app/init/datasources/Application.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate-groovy-proxy/src/test/groovy/example/ProxySpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/build.gradle create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/apple-touch-icon-retina.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/apple-touch-icon.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/favicon.ico create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/grails_logo.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/database_add.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/database_delete.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/database_edit.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/database_save.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/database_table.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/exclamation.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/house.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/information.png create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/shadow.jpg create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/sorted_asc.gif create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/skin/sorted_desc.gif create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/images/spinner.gif create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/javascripts/application.js create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/stylesheets/application.css create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/stylesheets/errors.css create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/stylesheets/main.css create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/assets/stylesheets/mobile.css create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/conf/spring/resources.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/controllers/functional/tests/BookController.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/controllers/functional/tests/ProductController.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/controllers/functional/tests/UrlMappings.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/domain/functional/tests/Book.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/domain/functional/tests/Business.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/domain/functional/tests/Employee.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/domain/functional/tests/Person.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/domain/functional/tests/Product.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_cs_CZ.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_da.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_de.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_es.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_fr.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_it.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_ja.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_nb.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_nl.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_pl.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_pt_BR.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_pt_PT.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_ru.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_sv.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_th.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/i18n/messages_zh_CN.properties create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/init/functional/tests/Application.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/init/functional/tests/BootStrap.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/services/functional/tests/BookService.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/views/book/create.gsp create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/views/book/edit.gsp create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/views/book/index.gsp create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/views/book/show.gsp create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/views/error.gsp create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/views/index.gsp create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/views/layouts/main.gsp create mode 100644 grails-test-examples/hibernate7/grails-hibernate/grails-app/views/notFound.gsp create mode 100644 grails-test-examples/hibernate7/grails-hibernate/src/integration-test/groovy/functional/tests/BookControllerSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/src/integration-test/groovy/functional/tests/CascadeValidationSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/src/integration-test/groovy/functional/tests/ProductSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/src/integration-test/groovy/functional/tests/pages/BookPages.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/src/main/groovy/another/Item.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/src/main/groovy/functional/tests/CustomHibernateMappingContextConfiguration.groovy create mode 100644 grails-test-examples/hibernate7/grails-hibernate/src/test/groovy/functional/tests/BookControllerUnitSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/build.gradle create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/grails-app/controllers/datasources/SecondaryBookController.groovy create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/grails-app/controllers/datasources/UrlMappings.groovy create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/grails-app/domain/ds2/Book.groovy create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/grails-app/domain/example/Book.groovy create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/grails-app/init/datasources/Application.groovy create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/grails-app/services/example/BookService.groovy create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/src/integration-test/groovy/functionaltests/MultiDataSourceWithSessionSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-multiple-datasources/src/integration-test/groovy/functionaltests/MultipleDataSourcesSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-multitenant-multi-datasource/build.gradle create mode 100644 grails-test-examples/hibernate7/grails-multitenant-multi-datasource/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/grails-multitenant-multi-datasource/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/grails-multitenant-multi-datasource/grails-app/domain/example/Metric.groovy create mode 100644 grails-test-examples/hibernate7/grails-multitenant-multi-datasource/grails-app/init/example/Application.groovy create mode 100644 grails-test-examples/hibernate7/grails-multitenant-multi-datasource/grails-app/services/example/MetricService.groovy create mode 100644 grails-test-examples/hibernate7/grails-multitenant-multi-datasource/src/integration-test/groovy/functionaltests/MultiTenantMultiDataSourceSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/build.gradle create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/apple-touch-icon-retina.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/apple-touch-icon.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/favicon.ico create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/grails_logo.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/database_add.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/database_delete.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/database_edit.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/database_save.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/database_table.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/exclamation.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/house.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/information.png create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/shadow.jpg create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/sorted_asc.gif create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/skin/sorted_desc.gif create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/images/spinner.gif create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/javascripts/application.js create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/stylesheets/application.css create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/stylesheets/errors.css create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/stylesheets/main.css create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/assets/stylesheets/mobile.css create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/controllers/example/BookController.groovy create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/controllers/example/UrlMappings.groovy create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/domain/example/Book.groovy create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_cs_CZ.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_da.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_de.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_es.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_fr.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_it.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_ja.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_nb.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_nl.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_pl.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_pt_BR.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_pt_PT.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_ru.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_sv.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_th.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/i18n/messages_zh_CN.properties create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/init/datasources/Application.groovy create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/services/example/AnotherBookService.groovy create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/services/example/BookService.groovy create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/views/book/create.gsp create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/views/book/edit.gsp create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/views/book/index.gsp create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/views/book/show.gsp create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/views/error.gsp create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/views/index.gsp create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/views/layouts/main.gsp create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/grails-app/views/notFound.gsp create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/src/integration-test/groovy/example/PartitionedMultiTenancyIntegrationSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-partitioned-multi-tenancy/src/test/groovy/example/PartitionedMultiTenancySpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/build.gradle create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/apple-touch-icon-retina.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/apple-touch-icon.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/favicon.ico create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/grails_logo.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/database_add.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/database_delete.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/database_edit.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/database_save.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/database_table.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/exclamation.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/house.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/information.png create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/shadow.jpg create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/sorted_asc.gif create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/skin/sorted_desc.gif create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/images/spinner.gif create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/javascripts/application.js create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/stylesheets/application.css create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/stylesheets/errors.css create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/stylesheets/main.css create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/assets/stylesheets/mobile.css create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/controllers/schemapertenant/BookController.groovy create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/controllers/schemapertenant/UrlMappings.groovy create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/domain/schemapertenant/Book.groovy create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_cs_CZ.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_da.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_de.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_es.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_fr.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_it.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_ja.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_nb.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_nl.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_pl.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_pt_BR.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_pt_PT.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_ru.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_sv.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_th.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/i18n/messages_zh_CN.properties create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/init/schemapertenant/Application.groovy create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/services/schemapertenant/AnotherBookService.groovy create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/services/schemapertenant/BookService.groovy create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/views/book/create.gsp create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/views/book/edit.gsp create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/views/book/index.gsp create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/views/book/show.gsp create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/views/error.gsp create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/views/index.gsp create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/views/layouts/main.gsp create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/grails-app/views/notFound.gsp create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/src/integration-test/groovy/schemapertenant/SchemaPerTenantIntegrationSpec.groovy create mode 100644 grails-test-examples/hibernate7/grails-schema-per-tenant/src/test/groovy/schemapertenant/SchemaPerTenantSpec.groovy create mode 100644 grails-test-examples/hibernate7/issue450/build.gradle create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/advancedgrails.svg create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/apple-touch-icon-retina.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/apple-touch-icon.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/documentation.svg create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/favicon.ico create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/grails-cupsonly-logo-white.svg create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/grails.svg create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/database_add.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/database_delete.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/database_edit.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/database_save.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/database_table.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/exclamation.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/house.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/information.png create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/shadow.jpg create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/sorted_asc.gif create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/skin/sorted_desc.gif create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/slack.svg create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/images/spinner.gif create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/javascripts/application.js create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/stylesheets/application.css create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/stylesheets/errors.css create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/stylesheets/grails.css create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/stylesheets/main.css create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/assets/stylesheets/mobile.css create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/conf/application.yml create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/conf/logback.xml create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/controllers/multitenantcomposite/BookController.groovy create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/controllers/multitenantcomposite/UrlMappings.groovy create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/domain/multitenantcomposite/Book.groovy create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/init/multitenantcomposite/Application.groovy create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/init/multitenantcomposite/BootStrap.groovy create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/services/multitenantcomposite/BookService.groovy create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/views/book/books.gsp create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/views/book/index.gsp create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/views/error.gsp create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/views/index.gsp create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/views/layouts/main.gsp create mode 100644 grails-test-examples/hibernate7/issue450/grails-app/views/notFound.gsp create mode 100644 grails-test-examples/hibernate7/issue450/src/integration-test/groovy/example/BookControllerSpec.groovy create mode 100644 grails-test-examples/hibernate7/spring-boot-hibernate/build.gradle create mode 100644 grails-test-examples/hibernate7/spring-boot-hibernate/src/main/groovy/example/Application.groovy create mode 100644 grails-test-examples/hibernate7/spring-boot-hibernate/src/main/groovy/example/Book.groovy create mode 100644 grails-test-examples/hibernate7/spring-boot-hibernate/src/main/groovy/example/BookController.groovy create mode 100644 grails-test-examples/hibernate7/spring-boot-hibernate/src/main/groovy/example/BookService.groovy create mode 100644 grails-test-examples/hibernate7/spring-boot-hibernate/src/main/resources/application.yml create mode 100644 grails-test-examples/hibernate7/spring-boot-hibernate/src/test/groovy/example/BookSpec.groovy create mode 100644 grails-test-examples/hibernate7/standalone-hibernate/build.gradle create mode 100644 grails-test-examples/hibernate7/standalone-hibernate/src/test/groovy/org/grails/hibernate/example/ExampleSpec.groovy create mode 100644 grails-test-examples/hibernate7/standalone-hibernate/src/test/resources/simplelogger.properties diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy index d707cfb9a6e..4258fcdeb57 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy @@ -122,6 +122,26 @@ class SbomPlugin implements Plugin { 'grails-data-hibernate5-dbmigration': [ 'pkg:maven/javax.xml.bind/jaxb-api@2.3.1?type=jar': 'CDDL-1.1', // api export ], + // hibernate7 staging: same LGPL exemptions as hibernate5 since the staging branch uses hibernate5 artifacts + 'grails-data-hibernate7-core' : [ + 'pkg:maven/org.hibernate.common/hibernate-commons-annotations@5.1.2.Final?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7 + 'pkg:maven/org.hibernate/hibernate-core-jakarta@5.6.15.Final?type=jar' : 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7 + ], + 'grails-data-hibernate7' : [ + 'pkg:maven/org.hibernate.common/hibernate-commons-annotations@5.1.2.Final?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7 + 'pkg:maven/org.hibernate/hibernate-core-jakarta@5.6.15.Final?type=jar' : 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7 + ], + 'grails-data-hibernate7-spring-boot': [ + 'pkg:maven/org.hibernate.common/hibernate-commons-annotations@5.1.2.Final?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7 + 'pkg:maven/org.hibernate/hibernate-core-jakarta@5.6.15.Final?type=jar' : 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7 + ], + 'grails-data-hibernate7-spring-orm' : [ + 'pkg:maven/org.hibernate.common/hibernate-commons-annotations@5.1.2.Final?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7 + 'pkg:maven/org.hibernate/hibernate-core-jakarta@5.6.15.Final?type=jar' : 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7 + ], + 'grails-data-hibernate7-dbmigration': [ + 'pkg:maven/javax.xml.bind/jaxb-api@2.3.1?type=jar': 'CDDL-1.1', // api export + ], ] @Override diff --git a/gradle/hibernate7-test-config.gradle b/gradle/hibernate7-test-config.gradle new file mode 100644 index 00000000000..1cf3f08392b --- /dev/null +++ b/gradle/hibernate7-test-config.gradle @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +dependencies { + // https://docs.gradle.org/8.3/userguide/upgrading_version_8.html#test_framework_implementation_dependencies + add('testRuntimeOnly', 'org.junit.platform:junit-platform-launcher') +} + +tasks.withType(Test).configureEach { + // Honor DO_NOT_CACHE_TESTS=1 so developers can repeatedly invoke the same test command + // without --rerun-tasks (and without recompiling everything else). + outputs.cacheIf { !doNotCacheTests } + outputs.upToDateWhen { !doNotCacheTests } + + onlyIf { + ![ + 'onlyFunctionalTests', + 'skipHibernate7Tests', + 'onlyMongodbTests', + 'onlyCoreTests', + 'skipTests' + ].find { + project.hasProperty(it) + } + } + + useJUnitPlatform() + systemProperty('hibernate7.gorm.suite', System.getProperty('hibernate7.gorm.suite') ?: true) + reports.html.required = !isCiBuild + reports.junitXml.required = !isCiBuild + testLogging { + events('passed', 'skipped', 'failed') + showExceptions = true + exceptionFormat = 'full' + showCauses = true + showStackTraces = true + + // set options for log level DEBUG and INFO + debug { + events('started', 'passed', 'skipped', 'failed', 'standardOut', 'standardError') + exceptionFormat = 'full' + } + info.events = debug.events + info.exceptionFormat = debug.exceptionFormat + } + afterTest { desc, result -> + logger.quiet(' -- Executed test {} [{}] with result: {}', desc.name, desc.className, result.resultType) + } + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" + def startItem = '| ', endItem = ' |' + def repeatLength = startItem.length() + output.length() + endItem.length() + def dashes = '-' * repeatLength + logger.quiet('\n{}\n{}{}{}\n{}', dashes, startItem, output, endItem, dashes) + } + } +} diff --git a/gradle/publish-root-config.gradle b/gradle/publish-root-config.gradle index f4801df7665..05578bdd7fc 100644 --- a/gradle/publish-root-config.gradle +++ b/gradle/publish-root-config.gradle @@ -33,6 +33,7 @@ def publishedProjects = [ 'grails-base-bom', 'grails-bom', 'grails-hibernate5-bom', + 'grails-hibernate7-bom', 'grails-micronaut-bom', 'grails-bootstrap', 'grails-cache', @@ -121,6 +122,12 @@ def publishedProjects = [ 'grails-data-hibernate5-dbmigration', 'grails-data-hibernate5-spring-boot', 'grails-data-hibernate5-spring-orm', + // hibernate7 + 'grails-data-hibernate7', + 'grails-data-hibernate7-core', + 'grails-data-hibernate7-dbmigration', + 'grails-data-hibernate7-spring-boot', + 'grails-data-hibernate7-spring-orm', // mongodb 'grails-data-mongodb', 'grails-data-mongodb-bson', diff --git a/grails-bom/hibernate7/build.gradle b/grails-bom/hibernate7/build.gradle new file mode 100644 index 00000000000..beba569941e --- /dev/null +++ b/grails-bom/hibernate7/build.gradle @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.grails.gradle.tasks.bom.ExtractDependenciesTask +import org.apache.grails.gradle.tasks.bom.ExtractedDependencyConstraint +import org.apache.grails.gradle.tasks.bom.PropertyNameCalculator + +buildscript { + apply from: rootProject.layout.projectDirectory.file('dependencies.gradle') +} + +plugins { + id 'java-platform' + id 'org.apache.grails.buildsrc.publish' + id 'org.apache.grails.buildsrc.sbom' +} + +version = projectVersion +group = 'org.apache.grails' + +javaPlatform { + allowDependencies() +} + +ext { + isReleaseBuild = System.getenv('GRAILS_PUBLISH_RELEASE') == 'true' + isPublishedExternal = System.getenv().containsKey('NEXUS_PUBLISH_STAGING_PROFILE_ID') + // TODO: It should be possible to pull these build names using includedBuild, but I haven't found a way to do so + gradleBuildProjects = [ + 'grails-gradle-plugins':'org.apache.grails', + 'grails-gradle-model':'org.apache.grails.gradle', + 'grails-gradle-common':'org.apache.grails.gradle', + 'grails-gradle-tasks':'org.apache.grails', + ] +} + +dependencies { + api platform(project(':grails-bom')) + api platform(gradleBomPlatformDependencies['spring-boot-bom']), { + exclude group: 'org.apache.groovy' + exclude group: 'org.hibernate.orm' + } + + constraints { + // Re-declare base BOM constraints directly so enforcedPlatform() consumers + // get forced versions. Constraints inherited via platform() are not enforced + // by enforcedPlatform — only direct constraints are. + for (def entry : gradleBomDependencies.entrySet()) { + api entry.value + } + for (def entry : bomDependencies.entrySet()) { + api entry.value + } + for (def entry : bomPlatformDependencies.entrySet()) { + api entry.value + } + for (def entry : customBomDependencies.entrySet()) { + def parts = entry.value.split(':') + if (parts.length == 3) { + api("${parts[0]}:${parts[1]}") { + version { + strictly parts[2] + } + } + } else { + api entry.value + } + } + } +} + +configurations.register('bomDependencies').configure { + it.canBeResolved = true + it.transitive = true + it.extendsFrom(configurations.named('api').get()) +} + +tasks.register('extractConstraints', ExtractDependenciesTask).configure { ExtractDependenciesTask it -> + it.captureProjectServices(project.dependencies, project.configurations) + it.configuration = configurations.named('bomDependencies') + it.configurationName = 'bomDependencies' + it.destination = project.layout.buildDirectory.file('grails-hibernate7-bom-constraints.adoc') + it.platformDefinitions = combinedPlatforms + it.definitions = combinedDependencies + it.projectName = project.name + it.versions = combinedVersions + rootProject.subprojects.each { p -> + evaluationDependsOn(p.path) + } + it.projectArtifactIds.set(project.provider { + Map artifactIdMappings = [:] + + rootProject.subprojects.each { p -> + artifactIdMappings[p.name] = p.findProperty('pomArtifactId') ?: p.name + } + + for (Map.Entry dependency : project.ext.gradleBuildProjects.entrySet()) { + artifactIdMappings[dependency.key] = dependency.key + } + + artifactIdMappings + }) + it.forcedGroupPrefixes.set(['org.apache.grails.profiles': 'grails-profile']) + it.projectCoordinateProperties.set(project.provider { + Map projectCoordinates = [:] + + rootProject.subprojects.each { p -> + String artifactId = p.findProperty('pomArtifactId') as String ?: p.name + String baseVersionName = artifactId.replaceAll('[.]', '-') + projectCoordinates["${p.group}:${ artifactId}:${p.version}" as String] = baseVersionName + } + + for (Map.Entry dependency : project.ext.gradleBuildProjects.entrySet()) { + projectCoordinates["${dependency.value}:${dependency.key}:${project.version}" as String] = dependency.key + } + + projectCoordinates + }) + + it.dependsOn(project.tasks.named('generateMetadataFileForMavenPublication'), project.tasks.named('generatePomFileForMavenPublication')) +} + +def validateNoSnapshotDependencies = tasks.register('validateNoSnapshotDependencies') +validateNoSnapshotDependencies.configure { Task it -> + it.group = 'publishing' + it.description = 'Validates that no snapshot dependencies are present in the project when performing a release.' + + it.doLast { + configurations.each { config -> + config.allDependencies.each { dep -> + if (dep.version && dep.version.contains('-SNAPSHOT')) { + throw new GradleException("Releases cannot have a snapshot dependency: ${dep.group}:${dep.name} (${dep.version})") + } + } + } + } +} + +if (ext.isReleaseBuild && ext.isPublishedExternal) { + project.afterEvaluate { + tasks.named('generateMetadataFileForMavenPublication').configure { + dependsOn(validateNoSnapshotDependencies) + } + tasks.named('generatePomFileForMavenPublication').configure { + dependsOn(validateNoSnapshotDependencies) + } + } +} + +ext { + pomDescription = 'Grails Hibernate 7 BOM (Bill of Materials) for managing dependency versions used by Grails Projects with Hibernate 7' + pomCustomization = { xml -> + def root = xml.asNode() + + def propertiesNode = root.properties ? root.properties[0] : root.appendNode('properties') + + def depMgmt = root.dependencyManagement?.getAt(0) + def deps = depMgmt?.dependencies?.getAt(0) + if (deps) { + PropertyNameCalculator propertyNameCalculator = new PropertyNameCalculator(combinedPlatforms, combinedDependencies, combinedVersions) + propertyNameCalculator.addForcedGroupPrefix('org.apache.grails.profiles', 'grails-profile') + propertyNameCalculator.addProjects(rootProject.subprojects) + for (String gradleArtifactId : project.ext.gradleBuildProjects) { + propertyNameCalculator.addProject('org.apache.grails.gradle', gradleArtifactId, project.version as String, gradleArtifactId) + } + + Map pomProperties = [:] + deps.dependency.each { dep -> + String groupId = dep.groupId.text().trim() + String artifactId = dep.artifactId.text().trim() + boolean isBom = dep.scope.text().trim() == 'import' + + String inlineVersion = dep.version.text().trim() + if (inlineVersion == 'null') { + inlineVersion = null + } + + if (inlineVersion) { + ExtractedDependencyConstraint extractedConstraint = propertyNameCalculator.calculate(groupId, artifactId, inlineVersion, isBom) + if (extractedConstraint?.versionPropertyReference) { + // use the property reference instead of the hard coded version so that it can be + // overriden by the spring boot dependency management plugin + dep.version[0].value = extractedConstraint.versionPropertyReference + + // Add an entry in the node with the actual version number + pomProperties.put(extractedConstraint.versionPropertyName, inlineVersion) + } + } else if (!inlineVersion) { + throw new GradleException("Dependency $groupId:$artifactId does not have a version.") + } + } + + for (Map.Entry property : pomProperties.entrySet()) { + propertiesNode.appendNode(property.key, property.value) + } + } + } +} diff --git a/grails-data-hibernate7/boot-plugin/build.gradle b/grails-data-hibernate7/boot-plugin/build.gradle new file mode 100644 index 00000000000..c4aa1e13854 --- /dev/null +++ b/grails-data-hibernate7/boot-plugin/build.gradle @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +plugins { + id 'groovy' + id 'java-library' + id 'org.apache.grails.buildsrc.properties' + id 'org.apache.grails.buildsrc.dependency-validator' + id 'org.apache.grails.buildsrc.compile' + id 'org.apache.grails.buildsrc.publish' + id 'org.apache.grails.buildsrc.sbom' + id 'org.apache.grails.gradle.grails-code-style' +} + +version = projectVersion +group = 'org.apache.grails' + +ext { + gormApiDocs = true + pomTitle = 'Grails GORM' + pomDescription = 'GORM - Grails Data Access Framework' +} + +dependencies { + // TODO: Clarify and clean up dependencies + implementation platform(project(':grails-bom')) + + api project(":grails-data-hibernate7-core") + api "org.apache.groovy:groovy" + api "org.springframework.boot:spring-boot-autoconfigure" + + compileOnly project(':grails-shell-cli'), { + exclude group:'org.apache.groovy', module:'groovy' + } + compileOnly "org.springframework.boot:spring-boot-jdbc" + compileOnly "org.springframework.boot:spring-boot-hibernate" + + testImplementation project(':grails-shell-cli'), { + exclude group:'org.apache.groovy', module:'groovy' + } + testImplementation "org.spockframework:spock-core" + testImplementation "org.springframework.boot:spring-boot-jdbc" + testImplementation "org.springframework.boot:spring-boot-hibernate" + + testRuntimeOnly "org.apache.tomcat:tomcat-jdbc" + testRuntimeOnly "com.h2database:h2" +} + +apply { + from rootProject.layout.projectDirectory.file('gradle/hibernate7-test-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') +} diff --git a/grails-data-hibernate7/boot-plugin/src/main/groovy/org/grails/datastore/gorm/boot/autoconfigure/HibernateGormAutoConfiguration.groovy b/grails-data-hibernate7/boot-plugin/src/main/groovy/org/grails/datastore/gorm/boot/autoconfigure/HibernateGormAutoConfiguration.groovy new file mode 100644 index 00000000000..5c074f50e24 --- /dev/null +++ b/grails-data-hibernate7/boot-plugin/src/main/groovy/org/grails/datastore/gorm/boot/autoconfigure/HibernateGormAutoConfiguration.groovy @@ -0,0 +1,136 @@ +/* Copyright (C) 2014 SpringSource + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.grails.datastore.gorm.boot.autoconfigure + +import java.beans.Introspector + +import javax.sql.DataSource + +import groovy.transform.CompileStatic + +import org.hibernate.SessionFactory + +import org.springframework.beans.BeansException +import org.springframework.beans.factory.BeanFactory +import org.springframework.beans.factory.BeanFactoryAware +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.boot.autoconfigure.AutoConfigurationPackages +import org.springframework.boot.autoconfigure.AutoConfigureAfter +import org.springframework.boot.autoconfigure.AutoConfigureBefore +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.transaction.PlatformTransactionManager + +import org.grails.datastore.gorm.events.ConfigurableApplicationContextEventPublisher +import org.grails.datastore.mapping.services.Service +import org.grails.orm.hibernate.HibernateDatastore +import org.grails.orm.hibernate.cfg.HibernateMappingContextConfiguration + +/** + * Auto configuration for GORM for Hibernate + * + * @author Graeme Rocher + * @since 1.0 + */ +@CompileStatic +@Configuration +@ConditionalOnClass(HibernateMappingContextConfiguration) +@ConditionalOnBean(DataSource) +@ConditionalOnMissingBean(type = 'grails.orm.bootstrap.HibernateDatastoreSpringInitializer') +@AutoConfigureAfter(DataSourceAutoConfiguration) +@AutoConfigureBefore([HibernateJpaAutoConfiguration]) +class HibernateGormAutoConfiguration implements ApplicationContextAware,BeanFactoryAware { + + BeanFactory beanFactory + + @Autowired(required = false) + DataSource dataSource + + ConfigurableApplicationContext applicationContext + + @Bean + HibernateDatastore hibernateDatastore() { + List packageNames = AutoConfigurationPackages.get(this.beanFactory) + List packages = [] + for (name in packageNames) { + Package pkg = Package.getPackage(name) + if (pkg != null) { + packages.add(pkg) + } + } + + ConfigurableListableBeanFactory beanFactory = applicationContext.beanFactory + HibernateDatastore datastore + if (dataSource == null) { + datastore = new HibernateDatastore( + applicationContext.getEnvironment(), + new ConfigurableApplicationContextEventPublisher(applicationContext), + packages as Package[] + ) + beanFactory.registerSingleton('dataSource', datastore.getDataSource()) + } + else { + datastore = new HibernateDatastore( + dataSource, + applicationContext.getEnvironment(), + new ConfigurableApplicationContextEventPublisher(applicationContext), + packages as Package[] + ) + } + + for (Service service in datastore.getServices()) { + Class serviceClass = service.getClass() + grails.gorm.services.Service ann = serviceClass.getAnnotation(grails.gorm.services.Service) + String serviceName = ann?.name() + if (serviceName == null) { + serviceName = Introspector.decapitalize(serviceClass.simpleName) + } + if (!applicationContext.containsBean(serviceName)) { + applicationContext.beanFactory.registerSingleton( + serviceName, + service + ) + } + } + return datastore + } + + @Bean + SessionFactory sessionFactory() { + hibernateDatastore().getSessionFactory() + } + + @Bean + PlatformTransactionManager hibernateTransactionManager() { + hibernateDatastore().getTransactionManager() + } + + @Override + void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (!(applicationContext instanceof ConfigurableApplicationContext)) { + throw new IllegalArgumentException('Neo4jAutoConfiguration requires an instance of ConfigurableApplicationContext') + } + this.applicationContext = (ConfigurableApplicationContext) applicationContext + } +} diff --git a/grails-data-hibernate7/boot-plugin/src/main/groovy/org/grails/datastore/gorm/boot/compiler/GormCompilerAutoConfiguration.groovy b/grails-data-hibernate7/boot-plugin/src/main/groovy/org/grails/datastore/gorm/boot/compiler/GormCompilerAutoConfiguration.groovy new file mode 100644 index 00000000000..32d7f22be7a --- /dev/null +++ b/grails-data-hibernate7/boot-plugin/src/main/groovy/org/grails/datastore/gorm/boot/compiler/GormCompilerAutoConfiguration.groovy @@ -0,0 +1,52 @@ +/* Copyright (C) 2014 SpringSource + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.grails.datastore.gorm.boot.compiler + +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.control.CompilationFailedException +import org.codehaus.groovy.control.customizers.ImportCustomizer + +import org.grails.cli.compiler.AstUtils +import org.grails.cli.compiler.CompilerAutoConfiguration +import org.grails.cli.compiler.DependencyCustomizer + +/** + * A compiler configuration that automatically adds the necessary imports + * + * @author Graeme Rocher + * @since 1.0 + * + */ +@CompileStatic +class GormCompilerAutoConfiguration extends CompilerAutoConfiguration { + + @Override + boolean matches(ClassNode classNode) { + return AstUtils.hasAtLeastOneAnnotation(classNode, 'grails.persistence.Entity', 'grails.gorm.annotation.Entity', 'Entity') + } + + @Override + void applyDependencies(DependencyCustomizer dependencies) throws CompilationFailedException { + dependencies.ifAnyMissingClasses('grails.persistence.Entity', 'grails.gorm.annotation.Entity') + .add('grails-data-hibernate5-core') + } + + @Override + void applyImports(ImportCustomizer imports) throws CompilationFailedException { + imports.addStarImports('grails.gorm', 'grails.gorm.annotation') + } +} diff --git a/grails-data-hibernate7/boot-plugin/src/main/resources/META-INF/services/org.grails.cli.compiler.CompilerAutoConfiguration b/grails-data-hibernate7/boot-plugin/src/main/resources/META-INF/services/org.grails.cli.compiler.CompilerAutoConfiguration new file mode 100644 index 00000000000..2e0f07984fe --- /dev/null +++ b/grails-data-hibernate7/boot-plugin/src/main/resources/META-INF/services/org.grails.cli.compiler.CompilerAutoConfiguration @@ -0,0 +1 @@ +org.grails.datastore.gorm.boot.compiler.GormCompilerAutoConfiguration \ No newline at end of file diff --git a/grails-data-hibernate7/boot-plugin/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/grails-data-hibernate7/boot-plugin/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..d93153f929c --- /dev/null +++ b/grails-data-hibernate7/boot-plugin/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.grails.datastore.gorm.boot.autoconfigure.HibernateGormAutoConfiguration diff --git a/grails-data-hibernate7/boot-plugin/src/test/groovy/org/grails/datastore/gorm/boot/autoconfigure/HibernateGormAutoConfigurationSpec.groovy b/grails-data-hibernate7/boot-plugin/src/test/groovy/org/grails/datastore/gorm/boot/autoconfigure/HibernateGormAutoConfigurationSpec.groovy new file mode 100644 index 00000000000..a84b5f703d7 --- /dev/null +++ b/grails-data-hibernate7/boot-plugin/src/test/groovy/org/grails/datastore/gorm/boot/autoconfigure/HibernateGormAutoConfigurationSpec.groovy @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.datastore.gorm.boot.autoconfigure + +import grails.gorm.annotation.Entity +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.boot.autoconfigure.AutoConfigurationPackages +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.core.env.MapPropertySource +import org.springframework.jdbc.datasource.DriverManagerDataSource +import spock.lang.Ignore +import spock.lang.Specification + +/** + * Created by graemerocher on 06/02/14. + */ +class HibernateGormAutoConfigurationSpec extends Specification{ + + protected AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + void cleanup() { + context.close() + } + + void setup() { + + AutoConfigurationPackages.register(context, "org.grails.datastore.gorm.boot.autoconfigure") + this.context.getEnvironment().getPropertySources().addFirst(new MapPropertySource("foo", ['hibernate.hbm2ddl.auto':'create'])) + def beanFactory = this.context.defaultListableBeanFactory + beanFactory.registerSingleton("dataSource", new DriverManagerDataSource("jdbc:h2:mem:grailsDb1;LOCK_TIMEOUT=10000;DB_CLOSE_DELAY=-1", 'sa', '')) + this.context.register( TestConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + } + + void 'Test that GORM is correctly configured'() { + when:"The context is refreshed" + context.refresh() + + def result = Person.withTransaction { + Person.count() + } + + then:"GORM queries work" + result == 0 + + when:"The addTo* methods are called" + def p = new Person() + p.addToChildren(firstName:"Bob") + + then:"They work too" + p.children.size() == 1 + } + + @Configuration + @Import(HibernateGormAutoConfiguration) + static class TestConfiguration { + } + +} + + +@Entity +class Person { + String firstName + String lastName + Integer age = 18 + + Set children = [] + static hasMany = [children: Person] +} + + diff --git a/grails-data-hibernate7/boot-plugin/src/test/groovy/org/springframework/bean/reader/GroovyBeanDefinitionReaderSpec.groovy b/grails-data-hibernate7/boot-plugin/src/test/groovy/org/springframework/bean/reader/GroovyBeanDefinitionReaderSpec.groovy new file mode 100644 index 00000000000..b762b73698f --- /dev/null +++ b/grails-data-hibernate7/boot-plugin/src/test/groovy/org/springframework/bean/reader/GroovyBeanDefinitionReaderSpec.groovy @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.springframework.bean.reader + +import org.springframework.beans.factory.InitializingBean +import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Specification + +/** + * Created by graemerocher on 06/02/14. + */ +class GroovyBeanDefinitionReaderSpec extends Specification{ + + protected AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + void setup() { + MyBean.blah = 'foo' + } + + void "Test singletons are pre-instantiated with beans added by GroovyBeanDefinitionReader"() { + when:"The groovy reader is used" + def beanReader= new GroovyBeanDefinitionReader(context) + beanReader.beans { + myBean(MyBean) + } + + context.refresh() + + then:"The bean is pre instantiated" + MyBean.blah == 'created' + } +} +class MyBean implements InitializingBean{ + static String blah = 'foo' + + @Override + void afterPropertiesSet() throws Exception { + blah = 'created' + } +} diff --git a/grails-data-hibernate7/core/build.gradle b/grails-data-hibernate7/core/build.gradle new file mode 100644 index 00000000000..bf1fe29e5be --- /dev/null +++ b/grails-data-hibernate7/core/build.gradle @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +plugins { + id 'groovy' + id 'java-library' + id 'org.apache.grails.buildsrc.properties' + id 'org.apache.grails.buildsrc.dependency-validator' + id 'org.apache.grails.buildsrc.compile' + id 'org.apache.grails.buildsrc.publish' + id 'org.apache.grails.buildsrc.sbom' + id 'org.apache.grails.gradle.grails-code-style' +} + +version = projectVersion +group = 'org.apache.grails.data' + +ext { + gormApiDocs = true + pomTitle = 'Grails GORM' + pomDescription = 'GORM - Grails Data Access Framework' +} + +dependencies { + // TODO: Clarify and clean up dependencies + implementation platform(project(':grails-bom')) + + api 'org.slf4j:slf4j-api' + + api 'org.apache.groovy:groovy' + api project(':grails-datamapping-core') + api project(':grails-data-hibernate7-spring-orm') + api 'org.springframework:spring-orm' + compileOnly 'jakarta.servlet:jakarta.servlet-api' + api 'org.hibernate:hibernate-core-jakarta', { + exclude group:'commons-logging', module:'commons-logging' + exclude group:'com.h2database', module:'h2' + exclude group:'commons-collections', module:'commons-collections' + exclude group:'org.slf4j', module:'jcl-over-slf4j' + exclude group:'org.slf4j', module:'slf4j-api' + exclude group:'org.slf4j', module:'slf4j-log4j12' + exclude group:'xml-apis', module:'xml-apis' + } + api 'org.hibernate.validator:hibernate-validator', { + exclude group:'commons-logging', module:'commons-logging' + exclude group:'commons-collections', module:'commons-collections' + exclude group:'org.slf4j', module:'slf4j-api' + } + + testImplementation 'com.h2database:h2' + testImplementation 'org.junit.platform:junit-platform-suite', { + // api: SelectClasses, Suite + } + testImplementation 'org.apache.groovy:groovy-test-junit5' + testImplementation 'org.apache.groovy:groovy-sql' + testImplementation 'org.apache.groovy:groovy-json' + testImplementation 'org.apache.tomcat:tomcat-jdbc' + testImplementation 'org.spockframework:spock-core' + testImplementation 'org.yakworks:hibernate-groovy-proxy', { + // groovy proxy fixes bytebuddy to be a bit smarter when it comes to groovy metaClass + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + + testRuntimeOnly 'org.hibernate:hibernate-ehcache', { + // exclude javax variant of hibernate-core 5.6 + exclude group: 'org.hibernate', module: 'hibernate-core' + } + testRuntimeOnly "org.jboss.spec.javax.transaction:jboss-transaction-api_1.3_spec:$jbossTransactionApiVersion", { + // required for hibernate-ehcache to work with javax variant of hibernate-core excluded + } + testRuntimeOnly 'org.slf4j:slf4j-simple' + testRuntimeOnly 'org.slf4j:jcl-over-slf4j' + testRuntimeOnly 'org.springframework:spring-aop' +} + +apply { + from rootProject.layout.projectDirectory.file('gradle/hibernate7-test-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/grails-data-tck-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') +} diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java new file mode 100644 index 00000000000..a4972c59244 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.orm; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import groovy.lang.GroovySystem; + +import jakarta.persistence.FetchType; +import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.PluralAttribute; + +import org.hibernate.Criteria; +import org.hibernate.SessionFactory; +import org.hibernate.criterion.Criterion; +import org.hibernate.criterion.Projection; +import org.hibernate.criterion.ProjectionList; +import org.hibernate.criterion.Projections; +import org.hibernate.sql.JoinType; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.Type; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import org.grails.datastore.gorm.query.criteria.AbstractDetachedCriteria; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.query.api.QueryableCriteria; +import org.grails.orm.hibernate.GrailsHibernateTemplate; +import org.grails.orm.hibernate.HibernateDatastore; +import org.grails.orm.hibernate.cfg.GrailsHibernateUtil; +import org.grails.orm.hibernate.query.AbstractHibernateCriteriaBuilder; +import org.grails.orm.hibernate.query.AbstractHibernateQuery; +import org.grails.orm.hibernate.query.HibernateProjectionAdapter; +import org.grails.orm.hibernate.query.HibernateQuery; +import org.grails.orm.hibernate.support.hibernate5.SessionHolder; + +/** + *

Wraps the Hibernate Criteria API in a builder. The builder can be retrieved through the "createCriteria()" dynamic static + * method of Grails domain classes (Example in Groovy): + *

+ *         def c = Account.createCriteria()
+ *         def results = c {
+ *             projections {
+ *                 groupProperty("branch")
+ *             }
+ *             like("holderFirstName", "Fred%")
+ *             and {
+ *                 between("balance", 500, 1000)
+ *                 eq("branch", "London")
+ *             }
+ *             maxResults(10)
+ *             order("holderLastName", "desc")
+ *         }
+ * 
+ *

The builder can also be instantiated standalone with a SessionFactory and persistent Class instance: + *

+ *      new HibernateCriteriaBuilder(clazz, sessionFactory).list {
+ *         eq("firstName", "Fred")
+ *      }
+ * 
+ * + * @author Graeme Rocher + */ +public class HibernateCriteriaBuilder extends AbstractHibernateCriteriaBuilder { + /* + * Define constants which may be used inside of criteria queries + * to refer to standard Hibernate Type instances. + */ + public static final Type BOOLEAN = StandardBasicTypes.BOOLEAN; + public static final Type YES_NO = StandardBasicTypes.YES_NO; + public static final Type BYTE = StandardBasicTypes.BYTE; + public static final Type CHARACTER = StandardBasicTypes.CHARACTER; + public static final Type SHORT = StandardBasicTypes.SHORT; + public static final Type INTEGER = StandardBasicTypes.INTEGER; + public static final Type LONG = StandardBasicTypes.LONG; + public static final Type FLOAT = StandardBasicTypes.FLOAT; + public static final Type DOUBLE = StandardBasicTypes.DOUBLE; + public static final Type BIG_DECIMAL = StandardBasicTypes.BIG_DECIMAL; + public static final Type BIG_INTEGER = StandardBasicTypes.BIG_INTEGER; + public static final Type STRING = StandardBasicTypes.STRING; + public static final Type NUMERIC_BOOLEAN = StandardBasicTypes.NUMERIC_BOOLEAN; + public static final Type TRUE_FALSE = StandardBasicTypes.TRUE_FALSE; + public static final Type URL = StandardBasicTypes.URL; + public static final Type TIME = StandardBasicTypes.TIME; + public static final Type DATE = StandardBasicTypes.DATE; + public static final Type TIMESTAMP = StandardBasicTypes.TIMESTAMP; + public static final Type CALENDAR = StandardBasicTypes.CALENDAR; + public static final Type CALENDAR_DATE = StandardBasicTypes.CALENDAR_DATE; + public static final Type CLASS = StandardBasicTypes.CLASS; + public static final Type LOCALE = StandardBasicTypes.LOCALE; + public static final Type CURRENCY = StandardBasicTypes.CURRENCY; + public static final Type TIMEZONE = StandardBasicTypes.TIMEZONE; + public static final Type UUID_BINARY = StandardBasicTypes.UUID_BINARY; + public static final Type UUID_CHAR = StandardBasicTypes.UUID_CHAR; + public static final Type BINARY = StandardBasicTypes.BINARY; + public static final Type WRAPPER_BINARY = StandardBasicTypes.WRAPPER_BINARY; + public static final Type IMAGE = StandardBasicTypes.IMAGE; + public static final Type BLOB = StandardBasicTypes.BLOB; + public static final Type MATERIALIZED_BLOB = StandardBasicTypes.MATERIALIZED_BLOB; + public static final Type CHAR_ARRAY = StandardBasicTypes.CHAR_ARRAY; + public static final Type CHARACTER_ARRAY = StandardBasicTypes.CHARACTER_ARRAY; + public static final Type TEXT = StandardBasicTypes.TEXT; + public static final Type CLOB = StandardBasicTypes.CLOB; + public static final Type MATERIALIZED_CLOB = StandardBasicTypes.MATERIALIZED_CLOB; + public static final Type SERIALIZABLE = StandardBasicTypes.SERIALIZABLE; + + @SuppressWarnings("rawtypes") + public HibernateCriteriaBuilder(Class targetClass, SessionFactory sessionFactory) { + super(targetClass, sessionFactory); + setDefaultFlushMode(GrailsHibernateTemplate.FLUSH_AUTO); + } + + @SuppressWarnings("rawtypes") + public HibernateCriteriaBuilder(Class targetClass, SessionFactory sessionFactory, boolean uniqueResult) { + super(targetClass, sessionFactory, uniqueResult); + setDefaultFlushMode(GrailsHibernateTemplate.FLUSH_AUTO); + } + + /** + * Join an association using the specified join-type, assigning an alias + * to the joined association. + * The joinType is expected to be one of CriteriaSpecification.INNER_JOIN (the default), + * CriteriaSpecificationFULL_JOIN, or CriteriaSpecificationLEFT_JOIN. + * + * @param associationPath A dot-seperated property path + * @param alias The alias to assign to the joined association (for later reference). + * @param joinType The type of join to use. + * @return this (for method chaining) + * @throws org.hibernate.HibernateException Indicates a problem creating the sub criteria + * @see #createAlias(String, String) + */ + public Criteria createAlias(String associationPath, String alias, int joinType) { + aliasMap.put(associationPath, alias); + return criteria.createAlias(associationPath, alias, JoinType.parse(joinType)); + } + + @Override + protected Object executeUniqueResultWithProxyUnwrap() { + return GrailsHibernateUtil.unwrapIfProxy(criteria.uniqueResult()); + } + + @Override + protected void cacheCriteriaMapping() { + GrailsHibernateUtil.cacheCriteriaByMapping(datastore, targetClass, criteria); + } + + protected Class getClassForAssociationType(Attribute type) { + if (type instanceof PluralAttribute) { + return ((PluralAttribute) type).getElementType().getJavaType(); + } + return type.getJavaType(); + } + + @Override + protected List createPagedResultList(Map args) { + GrailsHibernateUtil.populateArgumentsForCriteria(datastore, targetClass, criteria, args, conversionService); + GrailsHibernateTemplate ght = new GrailsHibernateTemplate(sessionFactory, (HibernateDatastore) datastore, getDefaultFlushMode()); + return new PagedResultList(ght, criteria); + } + + /** + * Creates a Criterion with from the specified property name and "rlike" (a regular expression version of "like") expression + * + * @param propertyName The property name + * @param propertyValue The ilike value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria rlike(String propertyName, Object propertyValue) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [rlike] with propertyName [" + + propertyName + "] and value [" + propertyValue + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + propertyValue = calculatePropertyValue(propertyValue); + addToCriteria(new RlikeExpression(propertyName, propertyValue)); + return this; + } + + @Override + protected void createCriteriaInstance() { + { + if (TransactionSynchronizationManager.hasResource(sessionFactory)) { + participate = true; + hibernateSession = ((SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory)).getSession(); + } + else { + hibernateSession = sessionFactory.openSession(); + } + + criteria = hibernateSession.createCriteria(targetClass); + cacheCriteriaMapping(); + criteriaMetaClass = GroovySystem.getMetaClassRegistry().getMetaClass(criteria.getClass()); + } + } + + @Override + protected org.hibernate.criterion.DetachedCriteria convertToHibernateCriteria(QueryableCriteria queryableCriteria) { + return getHibernateDetachedCriteria(new HibernateQuery(criteria, queryableCriteria.getPersistentEntity()), queryableCriteria); + } + + public static org.hibernate.criterion.DetachedCriteria getHibernateDetachedCriteria(AbstractHibernateQuery query, QueryableCriteria queryableCriteria) { + String alias = queryableCriteria.getAlias(); + return getHibernateDetachedCriteria(query, queryableCriteria, alias); + } + + public static org.hibernate.criterion.DetachedCriteria getHibernateDetachedCriteria(AbstractHibernateQuery query, QueryableCriteria queryableCriteria, String alias) { + PersistentEntity persistentEntity = queryableCriteria.getPersistentEntity(); + Class targetClass = persistentEntity.getJavaClass(); + org.hibernate.criterion.DetachedCriteria detachedCriteria; + + if (alias != null) { + detachedCriteria = org.hibernate.criterion.DetachedCriteria.forClass(targetClass, alias); + } + else { + detachedCriteria = org.hibernate.criterion.DetachedCriteria.forClass(targetClass); + } + populateHibernateDetachedCriteria(new HibernateQuery(detachedCriteria, persistentEntity), detachedCriteria, queryableCriteria); + return detachedCriteria; + } + + private static void populateHibernateDetachedCriteria(AbstractHibernateQuery query, org.hibernate.criterion.DetachedCriteria detachedCriteria, QueryableCriteria queryableCriteria) { + if (queryableCriteria instanceof AbstractDetachedCriteria) { + AbstractDetachedCriteria abstractDetachedCriteria = (AbstractDetachedCriteria) queryableCriteria; + Map fetchStrategies = abstractDetachedCriteria.getFetchStrategies(); + for (Entry entry : fetchStrategies.entrySet()) { + String property = entry.getKey(); + switch (entry.getValue()) { + case EAGER: + jakarta.persistence.criteria.JoinType gormJoinType = abstractDetachedCriteria.getJoinTypes().get(property); + if (gormJoinType != null) { + query.join(property, gormJoinType); + } + else { + query.join(property); + } + break; + case LAZY: + query.select(property); + break; + } + } + } + + List criteriaList = queryableCriteria.getCriteria(); + for (org.grails.datastore.mapping.query.Query.Criterion criterion : criteriaList) { + Criterion hibernateCriterion = HibernateQuery.HIBERNATE_CRITERION_ADAPTER.toHibernateCriterion(query, criterion, null); + if (hibernateCriterion != null) { + detachedCriteria.add(hibernateCriterion); + } + } + + List projections = queryableCriteria.getProjections(); + ProjectionList projectionList = Projections.projectionList(); + for (org.grails.datastore.mapping.query.Query.Projection projection : projections) { + Projection hibernateProjection = new HibernateProjectionAdapter(projection).toHibernateProjection(); + if (hibernateProjection != null) { + projectionList.add(hibernateProjection); + } + } + detachedCriteria.setProjection(projectionList); + } + + + /** + * Closes the session if it is copen + */ + @Override + protected void closeSession() { + if (hibernateSession != null && hibernateSession.isOpen() && !participate) { + hibernateSession.close(); + } + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/PagedResultList.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/PagedResultList.java new file mode 100644 index 00000000000..ad370bd638e --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/PagedResultList.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.orm; + +import java.sql.SQLException; +import java.util.Iterator; + +import org.hibernate.Criteria; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.criterion.Projections; +import org.hibernate.internal.CriteriaImpl; + +import org.grails.orm.hibernate.GrailsHibernateTemplate; +import org.grails.orm.hibernate.query.HibernateQuery; + +/** + * A result list for Criteria list calls, which is aware of the totalCount for + * the paged result. + * + * @author Siegfried Puchbauer + * @since 1.0 + * @deprecated Use {@link org.grails.orm.hibernate.query.PagedResultList} instead. + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +@Deprecated +public class PagedResultList extends grails.gorm.PagedResultList { + + private transient GrailsHibernateTemplate hibernateTemplate; + private final Criteria criteria; + + public PagedResultList(GrailsHibernateTemplate template, Criteria crit) { + super(null); + resultList = crit.list(); + criteria = crit; + hibernateTemplate = template; + } + + public PagedResultList(GrailsHibernateTemplate template, HibernateQuery query) { + super(null); + resultList = query.listForCriteria(); + criteria = query.getHibernateCriteria(); + hibernateTemplate = template; + } + + @Override + protected void initialize() { + // no-op, already initialized + } + + @Override + public int getTotalCount() { + if (totalCount == Integer.MIN_VALUE) { + totalCount = hibernateTemplate.execute(new GrailsHibernateTemplate.HibernateCallback<>() { + public Integer doInHibernate(Session session) throws HibernateException, SQLException { + CriteriaImpl impl = (CriteriaImpl) criteria; + Criteria totalCriteria = session.createCriteria(impl.getEntityOrClassName()); + hibernateTemplate.applySettings(totalCriteria); + + Iterator iterator = impl.iterateExpressionEntries(); + while (iterator.hasNext()) { + CriteriaImpl.CriterionEntry entry = (CriteriaImpl.CriterionEntry) iterator.next(); + totalCriteria.add(entry.getCriterion()); + } + Iterator subcriteriaIterator = impl.iterateSubcriteria(); + while (subcriteriaIterator.hasNext()) { + CriteriaImpl.Subcriteria sub = (CriteriaImpl.Subcriteria) subcriteriaIterator.next(); + totalCriteria.createAlias(sub.getPath(), sub.getAlias(), sub.getJoinType(), sub.getWithClause()); + } + totalCriteria.setProjection(impl.getProjection()); + totalCriteria.setProjection(Projections.rowCount()); + return ((Number) totalCriteria.uniqueResult()).intValue(); + } + }); + } + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/RlikeExpression.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/RlikeExpression.java new file mode 100644 index 00000000000..e2e6c0223d1 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/RlikeExpression.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.orm; + +import org.hibernate.Criteria; +import org.hibernate.HibernateException; +import org.hibernate.criterion.CriteriaQuery; +import org.hibernate.criterion.Criterion; +import org.hibernate.criterion.MatchMode; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.Oracle8iDialect; +import org.hibernate.dialect.PostgreSQL81Dialect; +import org.hibernate.engine.spi.TypedValue; + +/** + * Adds support for rlike to Hibernate in supported dialects. + * + * @author Graeme Rocher + * @since 1.1.1 + */ +public class RlikeExpression implements Criterion { + + private static final long serialVersionUID = -214329918050957956L; + + private final String propertyName; + private final Object value; + + public RlikeExpression(String propertyName, Object value) { + this.propertyName = propertyName; + this.value = value; + } + + public RlikeExpression(String propertyName, String value, MatchMode matchMode) { + this(propertyName, matchMode.toMatchString(value)); + } + + public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { + Dialect dialect = criteriaQuery.getFactory().getDialect(); + String[] columns = criteriaQuery.getColumnsUsingProjection(criteria, propertyName); + if (columns.length != 1) { + throw new HibernateException("rlike may only be used with single-column properties"); + } + + if (dialect instanceof MySQLDialect) { + return columns[0] + " rlike ?"; + } + + if (isOracleDialect(dialect)) { + return " REGEXP_LIKE (" + columns[0] + ", ?)"; + } + + if (dialect instanceof PostgreSQL81Dialect) { + return columns[0] + " ~* ?"; + } + + if (dialect instanceof H2Dialect) { + return columns[0] + " REGEXP ?"; + } + + throw new HibernateException("rlike is not supported with the configured dialect " + dialect.getClass().getCanonicalName()); + } + + private boolean isOracleDialect(Dialect dialect) { + return (dialect instanceof Oracle8iDialect); + } + + public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { + return new TypedValue[] { criteriaQuery.getTypedValue(criteria, propertyName, value.toString()) }; + } + + @Override + public String toString() { + return propertyName + " rlike " + value; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/HibernateEntity.groovy b/grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/HibernateEntity.groovy new file mode 100644 index 00000000000..555a69c0615 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/HibernateEntity.groovy @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.hibernate + +import groovy.transform.CompileStatic +import groovy.transform.Generated + +import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormEntity +import org.grails.orm.hibernate.AbstractHibernateGormStaticApi + +/** + * Extends the {@link GormEntity} trait adding additional Hibernate specific methods + * + * @author Graeme Rocher + * @since 6.1 + */ +@CompileStatic +trait HibernateEntity extends GormEntity { + + /** + * Finds all objects for the given string-based query + * + * @param sql The query + * + * @return The object + */ + @Generated + static List findAllWithSql(CharSequence sql) { + currentHibernateStaticApi().findAllWithSql(sql, Collections.emptyMap()) + } + + /** + * Finds an entity for the given SQL query + * + * @param sql The sql query + * @return The entity + */ + @Generated + static D findWithSql(CharSequence sql) { + currentHibernateStaticApi().findWithSql(sql, Collections.emptyMap()) + } + + /** + * Finds all objects for the given string-based query + * + * @param sql The query + * + * @return The object + */ + @Generated + static List findAllWithSql(CharSequence sql, Map args) { + currentHibernateStaticApi().findAllWithSql(sql, args) + } + + /** + * Finds an entity for the given SQL query + * + * @param sql The sql query + * @return The entity + */ + @Generated + static D findWithSql(CharSequence sql, Map args) { + currentHibernateStaticApi().findWithSql(sql, args) + } + + @Generated + private static AbstractHibernateGormStaticApi currentHibernateStaticApi() { + (AbstractHibernateGormStaticApi) GormEnhancer.findStaticApi(this) + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/annotation/ManagedEntity.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/annotation/ManagedEntity.java new file mode 100644 index 00000000000..874efe9f95c --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/annotation/ManagedEntity.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.hibernate.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.codehaus.groovy.transform.GroovyASTTransformationClass; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.grails.orm.hibernate.compiler.HibernateEntityTransformation") +public @interface ManagedEntity { + // no attributes +} diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/mapping/MappingBuilder.groovy b/grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/mapping/MappingBuilder.groovy new file mode 100644 index 00000000000..2de0609c2c5 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/hibernate/mapping/MappingBuilder.groovy @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.hibernate.mapping + +import groovy.transform.CompileStatic + +import org.grails.datastore.mapping.config.MappingDefinition +import org.grails.orm.hibernate.cfg.Mapping +import org.grails.orm.hibernate.cfg.PropertyConfig + +/** + * Entry point for the ORM mapping configuration DSL + * + * @author Graeme Rocher + * @since 6.1 + */ +@CompileStatic +class MappingBuilder { + + /** + * Build a Hibernate mapping + * + * @param mappingDefinition The closure defining the mapping + * @return The mapping + */ + static MappingDefinition define(@DelegatesTo(Mapping) Closure mappingDefinition) { + new ClosureMappingDefinition(mappingDefinition) + } + + /** + * Build a Hibernate mapping + * + * @param mappingDefinition The closure defining the mapping + * @return The mapping + */ + static MappingDefinition orm(@DelegatesTo(Mapping) Closure mappingDefinition) { + new ClosureMappingDefinition(mappingDefinition) + } + + @CompileStatic + private static class ClosureMappingDefinition implements MappingDefinition { + final Closure definition + private Mapping mapping + + ClosureMappingDefinition(Closure definition) { + this.definition = definition + } + + @Override + Mapping configure(Mapping existing) { + return Mapping.configureExisting(existing, definition) + } + + @Override + Mapping build() { + if (mapping == null) { + mapping = Mapping.configureNew(definition) + } + return mapping + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateDatastore.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateDatastore.java new file mode 100644 index 00000000000..0d62627c39e --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateDatastore.java @@ -0,0 +1,444 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; + +import javax.sql.DataSource; + +import groovy.lang.Closure; + +import jakarta.annotation.PreDestroy; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.core.env.PropertyResolver; + +import grails.gorm.multitenancy.Tenants; +import org.grails.datastore.gorm.events.AutoTimestampEventListener; +import org.grails.datastore.gorm.jdbc.schema.DefaultSchemaHandler; +import org.grails.datastore.gorm.jdbc.schema.SchemaHandler; +import org.grails.datastore.gorm.validation.registry.support.ValidatorRegistries; +import org.grails.datastore.mapping.config.Settings; +import org.grails.datastore.mapping.core.AbstractDatastore; +import org.grails.datastore.mapping.core.Datastore; +import org.grails.datastore.mapping.core.DatastoreAware; +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.datastore.mapping.core.connections.ConnectionSources; +import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore; +import org.grails.datastore.mapping.core.connections.SingletonConnectionSources; +import org.grails.datastore.mapping.model.MappingContext; +import org.grails.datastore.mapping.model.config.GormProperties; +import org.grails.datastore.mapping.multitenancy.AllTenantsResolver; +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings; +import org.grails.datastore.mapping.multitenancy.SchemaMultiTenantCapableDatastore; +import org.grails.datastore.mapping.multitenancy.TenantResolver; +import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException; +import org.grails.datastore.mapping.multitenancy.resolvers.FixedTenantResolver; +import org.grails.datastore.mapping.transactions.TransactionCapableDatastore; +import org.grails.datastore.mapping.validation.ValidatorRegistry; +import org.grails.orm.hibernate.cfg.HibernateMappingContext; +import org.grails.orm.hibernate.connections.HibernateConnectionSource; +import org.grails.orm.hibernate.connections.HibernateConnectionSourceSettings; +import org.grails.orm.hibernate.event.listener.AbstractHibernateEventListener; + +/** + * Datastore implementation that uses a Hibernate SessionFactory underneath. + * + * @author Graeme Rocher + * @since 2.0 + */ +public abstract class AbstractHibernateDatastore extends AbstractDatastore implements ApplicationContextAware, Settings, SchemaMultiTenantCapableDatastore, TransactionCapableDatastore, Closeable, MessageSourceAware, MultipleConnectionSourceCapableDatastore { + + public static final String CONFIG_PROPERTY_CACHE_QUERIES = "grails.hibernate.cache.queries"; + public static final String CONFIG_PROPERTY_OSIV_READONLY = "grails.hibernate.osiv.readonly"; + public static final String CONFIG_PROPERTY_PASS_READONLY_TO_HIBERNATE = "grails.hibernate.pass.readonly"; + protected final SessionFactory sessionFactory; + protected final ConnectionSources connectionSources; + protected final String defaultFlushModeName; + protected final MultiTenancySettings.MultiTenancyMode multiTenantMode; + protected final SchemaHandler schemaHandler; + protected AbstractHibernateEventListener eventTriggeringInterceptor; + protected AutoTimestampEventListener autoTimestampEventListener; + protected final boolean osivReadOnly; + protected final boolean passReadOnlyToHibernate; + protected final boolean isCacheQueries; + protected final int defaultFlushMode; + protected final boolean failOnError; + protected final boolean markDirty; + protected final String dataSourceName; + protected final TenantResolver tenantResolver; + private boolean destroyed; + + protected AbstractHibernateDatastore(ConnectionSources connectionSources, HibernateMappingContext mappingContext) { + super(mappingContext, connectionSources.getBaseConfiguration(), null); + this.connectionSources = connectionSources; + final HibernateConnectionSource defaultConnectionSource = (HibernateConnectionSource) connectionSources.getDefaultConnectionSource(); + this.dataSourceName = defaultConnectionSource.getName(); + this.sessionFactory = defaultConnectionSource.getSource(); + HibernateConnectionSourceSettings settings = defaultConnectionSource.getSettings(); + HibernateConnectionSourceSettings.HibernateSettings hibernateSettings = settings.getHibernate(); + this.osivReadOnly = hibernateSettings.getOsiv().isReadonly(); + this.passReadOnlyToHibernate = hibernateSettings.isReadOnly(); + this.isCacheQueries = hibernateSettings.getCache().isQueries(); + this.failOnError = settings.isFailOnError(); + Boolean markDirty = settings.getMarkDirty(); + this.markDirty = markDirty == null ? false : markDirty; + FlushMode flushMode = FlushMode.valueOf(hibernateSettings.getFlush().getMode().name()); + this.defaultFlushModeName = flushMode.name(); + this.defaultFlushMode = flushMode.getLevel(); + + MultiTenancySettings multiTenancySettings = settings.getMultiTenancy(); + final TenantResolver multiTenantResolver = multiTenancySettings.getTenantResolver(); + + this.multiTenantMode = multiTenancySettings.getMode(); + Class schemaHandlerClass = settings.getDataSource().getSchemaHandler(); + this.schemaHandler = BeanUtils.instantiateClass(schemaHandlerClass); + this.tenantResolver = multiTenantResolver; + if (multiTenantResolver instanceof DatastoreAware) { + ((DatastoreAware) multiTenantResolver).setDatastore(this); + } + } + + protected AbstractHibernateDatastore(MappingContext mappingContext, SessionFactory sessionFactory, PropertyResolver config, ApplicationContext applicationContext, String dataSourceName) { + super(mappingContext, config, (ConfigurableApplicationContext) applicationContext); + this.connectionSources = new SingletonConnectionSources<>(new HibernateConnectionSource(dataSourceName, sessionFactory, null, null), config); + this.sessionFactory = sessionFactory; + this.dataSourceName = dataSourceName; + initializeConverters(mappingContext); + if (applicationContext != null) { + setApplicationContext(applicationContext); + } + + osivReadOnly = config.getProperty(CONFIG_PROPERTY_OSIV_READONLY, Boolean.class, false); + passReadOnlyToHibernate = config.getProperty(CONFIG_PROPERTY_PASS_READONLY_TO_HIBERNATE, Boolean.class, false); + isCacheQueries = config.getProperty(CONFIG_PROPERTY_CACHE_QUERIES, Boolean.class, false); + + if (config.getProperty(SETTING_AUTO_FLUSH, Boolean.class, false)) { + this.defaultFlushModeName = FlushMode.AUTO.name(); + defaultFlushMode = FlushMode.AUTO.level; + } + else { + FlushMode flushMode = config.getProperty(SETTING_FLUSH_MODE, FlushMode.class, FlushMode.COMMIT); + this.defaultFlushModeName = flushMode.name(); + defaultFlushMode = flushMode.level; + } + failOnError = config.getProperty(SETTING_FAIL_ON_ERROR, Boolean.class, false); + markDirty = config.getProperty(SETTING_MARK_DIRTY, Boolean.class, false); + this.tenantResolver = new FixedTenantResolver(); + this.multiTenantMode = MultiTenancySettings.MultiTenancyMode.NONE; + this.schemaHandler = new DefaultSchemaHandler(); + } + + public AbstractHibernateDatastore(MappingContext mappingContext, SessionFactory sessionFactory, PropertyResolver config) { + this(mappingContext, sessionFactory, config, null, ConnectionSource.DEFAULT); + } + + @Override + public void setMessageSource(MessageSource messageSource) { + ValidatorRegistry validatorRegistry = createValidatorRegistry(messageSource); + this.mappingContext.setValidatorRegistry( + validatorRegistry + ); + } + + protected ValidatorRegistry createValidatorRegistry(MessageSource messageSource) { + return ValidatorRegistries.createValidatorRegistry(mappingContext, getConnectionSources().getDefaultConnectionSource().getSettings(), messageSource); + } + + @Override + public MultiTenancySettings.MultiTenancyMode getMultiTenancyMode() { + return this.multiTenantMode == MultiTenancySettings.MultiTenancyMode.SCHEMA ? MultiTenancySettings.MultiTenancyMode.DATABASE : this.multiTenantMode; + } + + @Override + public Datastore getDatastoreForTenantId(Serializable tenantId) { + if (getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DATABASE) { + return getDatastoreForConnection(tenantId.toString()); + } + else { + return this; + } + } + + @Override + public TenantResolver getTenantResolver() { + return this.tenantResolver; + } + + @Override + public ConnectionSources getConnectionSources() { + return this.connectionSources; + } + + /** + * Obtain a child datastore for the given connection name + * + * @param connectionName The name of the connection + * @return The child data store + */ + public abstract AbstractHibernateDatastore getDatastoreForConnection(String connectionName); + + public Iterable resolveTenantIds() { + if (this.tenantResolver instanceof AllTenantsResolver) { + return ((AllTenantsResolver) tenantResolver).resolveTenantIds(); + } + else if (this.multiTenantMode == MultiTenancySettings.MultiTenancyMode.DATABASE) { + List tenantIds = new ArrayList<>(); + for (ConnectionSource connectionSource : this.connectionSources.getAllConnectionSources()) { + if (!ConnectionSource.DEFAULT.equals(connectionSource.getName())) { + tenantIds.add(connectionSource.getName()); + } + } + return tenantIds; + } + else { + return Collections.emptyList(); + } + } + + public Serializable resolveTenantIdentifier() throws TenantNotFoundException { + return Tenants.currentId(this); + } + + public boolean isAutoFlush() { + return defaultFlushMode == FlushMode.AUTO.level; + } + + /** + * @return Obtains the default flush mode level + */ + public int getDefaultFlushMode() { + return defaultFlushMode; + } + + /** + * @return The name of the default value flush + */ + public String getDefaultFlushModeName() { + return defaultFlushModeName; + } + + public boolean isFailOnError() { + return failOnError; + } + + public boolean isOsivReadOnly() { + return osivReadOnly; + } + + public boolean isPassReadOnlyToHibernate() { + return passReadOnlyToHibernate; + } + + public boolean isCacheQueries() { + return isCacheQueries; + } + + /** + * @return The Hibernate {@link SessionFactory} being used by this datastore instance + */ + public SessionFactory getSessionFactory() { + return sessionFactory; + } + + /** + * @return The {@link DataSource} being used by this datastore instance + */ + public DataSource getDataSource() { + return ((HibernateConnectionSource) this.connectionSources.getDefaultConnectionSource()).getDataSource(); + } + + // for testing + public AbstractHibernateEventListener getEventTriggeringInterceptor() { + return eventTriggeringInterceptor; + } + + /** + * @return The event listener that populates lastUpdated and dateCreated + */ + public AutoTimestampEventListener getAutoTimestampEventListener() { + return autoTimestampEventListener; + } + + /** + * @return The data source name being used + */ + public String getDataSourceName() { + return this.dataSourceName; + } + + /** + * Execute the given operation with the given flush mode + * + * @param flushMode + * @param callable The callable + */ + public abstract void withFlushMode(FlushMode flushMode, Callable callable); + + /** + * We use a separate enum here because the classes differ between Hibernate 3 and 4 + * + * @see org.hibernate.FlushMode + */ + public enum FlushMode { + MANUAL(0), + COMMIT(5), + AUTO(10), + ALWAYS(20); + + private final int level; + + FlushMode(int level) { + this.level = level; + } + + public int getLevel() { + return level; + } + } + + @Override + public void destroy() { + if (!this.destroyed) { + super.destroy(); + AbstractHibernateGormInstanceApi.resetInsertActive(); + try { + connectionSources.close(); + } catch (IOException e) { + LOG.error("There was an error shutting down GORM for an entity: " + e.getMessage(), e); + } + destroyed = true; + } + } + + @Override + @PreDestroy + public void close() { + try { + destroy(); + } catch (Exception e) { + LOG.error("Error closing hibernate datastore: " + e.getMessage(), e); + } + } + + /** + * Obtains a hibernate template for the given flush mode + * + * @param flushMode The flush mode + * @return The IHibernateTemplate + */ + public abstract IHibernateTemplate getHibernateTemplate(int flushMode); + + public IHibernateTemplate getHibernateTemplate() { + return getHibernateTemplate(defaultFlushMode); + } + + /** + * @return Opens a session + */ + public abstract Session openSession(); + + @Override + public T withSession(final Closure callable) { + Closure multiTenantCallable = prepareMultiTenantClosure(callable); + return getHibernateTemplate().execute(multiTenantCallable); + } + + public T withNewSession(final Closure callable) { + Closure multiTenantCallable = prepareMultiTenantClosure(callable); + return getHibernateTemplate().executeWithNewSession(multiTenantCallable); + } + + @Override + public T1 withNewSession(Serializable tenantId, Closure callable) { + if (getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DATABASE) { + AbstractHibernateDatastore datastore = getDatastoreForConnection(tenantId.toString()); + SessionFactory sessionFactory = datastore.getSessionFactory(); + + return datastore.getHibernateTemplate().executeWithExistingOrCreateNewSession(sessionFactory, callable); + } + else { + return withNewSession(callable); + } + } + + /** + * Enable the tenant id filter for the given datastore and entity + * + */ + public void enableMultiTenancyFilter() { + Serializable currentId = Tenants.currentId(this); + if (ConnectionSource.DEFAULT.equals(currentId)) { + disableMultiTenancyFilter(); + } + else { + getHibernateTemplate() + .getSessionFactory() + .getCurrentSession() + .enableFilter(GormProperties.TENANT_IDENTITY) + .setParameter(GormProperties.TENANT_IDENTITY, currentId); + } + } + + /** + * Disable the tenant id filter for the given datastore and entity + */ + public void disableMultiTenancyFilter() { + getHibernateTemplate() + .getSessionFactory() + .getCurrentSession() + .disableFilter(GormProperties.TENANT_IDENTITY); + } + + protected Closure prepareMultiTenantClosure(final Closure callable) { + final boolean isMultiTenant = getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR; + Closure multiTenantCallable; + if (isMultiTenant) { + multiTenantCallable = new Closure<>(this) { + @Override + public T call(Object... args) { + enableMultiTenancyFilter(); + try { + return callable.call(args); + } finally { + disableMultiTenancyFilter(); + } + } + }; + } + else { + multiTenantCallable = callable; + } + return multiTenantCallable; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormInstanceApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormInstanceApi.groovy new file mode 100644 index 00000000000..5b7c18d66bc --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormInstanceApi.groovy @@ -0,0 +1,491 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import org.hibernate.FlushMode +import org.hibernate.HibernateException +import org.hibernate.LockMode +import org.hibernate.Session +import org.hibernate.SessionFactory + +import org.springframework.beans.BeanWrapperImpl +import org.springframework.beans.InvalidPropertyException +import org.springframework.dao.DataAccessException +import org.springframework.validation.Errors +import org.springframework.validation.Validator + +import grails.gorm.validation.CascadingValidator +import org.grails.datastore.gorm.GormInstanceApi +import org.grails.datastore.gorm.GormValidateable +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.dirty.checking.DirtyCheckable +import org.grails.datastore.mapping.engine.event.ValidationEvent +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.config.GormProperties +import org.grails.datastore.mapping.model.types.Association +import org.grails.datastore.mapping.model.types.Embedded +import org.grails.datastore.mapping.model.types.ToOne +import org.grails.datastore.mapping.proxy.ProxyHandler +import org.grails.datastore.mapping.reflect.ClassUtils +import org.grails.datastore.mapping.reflect.EntityReflector +import org.grails.orm.hibernate.support.HibernateRuntimeUtils + +/** + * Abstract extension of the {@link GormInstanceApi} class that provides common logic shared by Hibernate 3 and Hibernate 4 + * + * @author Graeme Rocher + * @param + */ +@CompileStatic +abstract class AbstractHibernateGormInstanceApi extends GormInstanceApi { + + private static final String ARGUMENT_VALIDATE = 'validate' + private static final String ARGUMENT_DEEP_VALIDATE = 'deepValidate' + private static final String ARGUMENT_FLUSH = 'flush' + private static final String ARGUMENT_INSERT = 'insert' + private static final String ARGUMENT_MERGE = 'merge' + private static final String ARGUMENT_FAIL_ON_ERROR = 'failOnError' + private static final Class DEFERRED_BINDING + + static { + try { + DEFERRED_BINDING = Class.forName('grails.validation.DeferredBindingActions') + } catch (Throwable e) { + DEFERRED_BINDING = null + } + } + + protected static final Object[] EMPTY_ARRAY = [] + /** + * When a domain instance is saved without validation, we put it + * into this thread local variable. Any code that needs to know + * whether the domain instance should be validated can just check + * the value. Note that this only works because the session is + * flushed when a domain instance is saved without validation. + */ + static final ThreadLocal insertActiveThreadLocal = new ThreadLocal() + + protected SessionFactory sessionFactory + protected ClassLoader classLoader + protected IHibernateTemplate hibernateTemplate + protected ProxyHandler proxyHandler + + boolean autoFlush + + protected AbstractHibernateGormInstanceApi(Class persistentClass, AbstractHibernateDatastore datastore, ClassLoader classLoader, IHibernateTemplate hibernateTemplate) { + super(persistentClass, datastore) + this.classLoader = classLoader + sessionFactory = datastore.getSessionFactory() + this.hibernateTemplate = hibernateTemplate + this.proxyHandler = datastore.mappingContext.getProxyHandler() + this.autoFlush = datastore.autoFlush + this.failOnError = datastore.failOnError + this.markDirty = datastore.markDirty + } + + @Override + D save(D target, Map arguments) { + + PersistentEntity domainClass = persistentEntity + runDeferredBinding() + boolean shouldFlush = shouldFlush(arguments) + boolean shouldValidate = shouldValidate(arguments, persistentEntity) + + HibernateRuntimeUtils.autoAssociateBidirectionalOneToOnes(domainClass, target) + + boolean deepValidate = true + if (arguments?.containsKey(ARGUMENT_DEEP_VALIDATE)) { + deepValidate = ClassUtils.getBooleanFromMap(ARGUMENT_DEEP_VALIDATE, arguments) + } + + if (shouldValidate) { + Validator validator = datastore.mappingContext.getEntityValidator(domainClass) + + Errors errors = HibernateRuntimeUtils.setupErrorsProperty(target) + + if (validator) { + datastore.applicationEventPublisher?.publishEvent(new ValidationEvent(datastore, target)) + + if (validator instanceof CascadingValidator) { + ((CascadingValidator) validator).validate(target, errors, deepValidate) + } else if (validator instanceof org.grails.datastore.gorm.validation.CascadingValidator) { + ((org.grails.datastore.gorm.validation.CascadingValidator) validator).validate(target, errors, deepValidate) + } else { + validator.validate(target, errors) + } + + if (errors.hasErrors()) { + handleValidationError(domainClass, target, errors) + if (shouldFail(arguments)) { + throw validationException.newInstance('Validation Error(s) occurred during save()', errors) + } + return null + } + + setObjectToReadWrite(target) + } + } + + // this piece of code will retrieve a persistent instant + // of a domain class property is only the id is set thus + // relieving this burden off the developer + autoRetrieveAssociations(datastore, domainClass, target) + + // Once we get here we've either validated this object or skipped validation, either way + // we don't need to validate again for the rest of this save. + GormValidateable validateable = (GormValidateable) target + validateable.skipValidation(true) + + try { + if (shouldInsert(arguments)) { + return performInsert(target, shouldFlush) + } + else if (shouldMerge(arguments)) { + return performMerge(target, shouldFlush) + } + else { + if (target instanceof DirtyCheckable && markDirty) { + target.markDirty() + } + return performSave(target, shouldFlush) + } + } finally { + // After save, we have to make sure this entity is setup to validate again. It's possible it will + // be validated again if this save didn't flush, but without checking it's dirty state we can't really + // know for sure that it hasn't changed and need to err on the side of caution. + validateable.skipValidation(false) + } + } + + @CompileDynamic + private void runDeferredBinding() { + DEFERRED_BINDING?.runActions() + } + + @Override + D merge(D instance, Map params) { + Map args = new HashMap(params) + args[ARGUMENT_MERGE] = true + return save(instance, args) + } + + @Override + D insert(D instance, Map params) { + Map args = new HashMap(params) + args[ARGUMENT_INSERT] = true + return save(instance, args) + } + + @Override + void discard(D instance) { + hibernateTemplate.evict(instance) + } + + @Override + void delete(D instance, Map params = Collections.emptyMap()) { + boolean flush = shouldFlush(params) + try { + hibernateTemplate.execute { Session session -> + session.delete(instance) + if (flush) { + session.flush() + } + } + } + catch (DataAccessException e) { + try { + hibernateTemplate.execute { Session session -> + session.flushMode = FlushMode.MANUAL + } + } + finally { + throw e + } + } + } + + @Override + boolean isAttached(D instance) { + hibernateTemplate.contains(instance) + } + + @Override + boolean instanceOf(D instance, Class cls) { + return proxyHandler.unwrap(instance) in cls + } + + @Override + D lock(D instance) { + hibernateTemplate.lock(instance, LockMode.PESSIMISTIC_WRITE) + instance + } + + @Override + D attach(D instance) { + hibernateTemplate.lock(instance, LockMode.NONE) + return instance + } + + @Override + D refresh(D instance) { + hibernateTemplate.refresh(instance) + return instance + } + + protected D performSave(final D target, final boolean flush) { + hibernateTemplate.execute { Session session -> + session.saveOrUpdate(target) + if (flush) { + flushSession(session) + } + return target + } + } + + protected D performMerge(final D target, final boolean flush) { + hibernateTemplate.execute { Session session -> + Object merged = session.merge(target) + session.lock(merged, LockMode.NONE) + if (flush) { + flushSession(session) + } + return (D) merged + } + } + + protected D performInsert(final D target, final boolean shouldFlush) { + hibernateTemplate.execute { Session session -> + try { + markInsertActive() + session.save(target) + if (shouldFlush) { + flushSession(session) + } + return target + } finally { + resetInsertActive() + } + + } + } + + protected void flushSession(Session session) throws HibernateException { + try { + session.flush() + } catch (HibernateException e) { + // session should not be flushed again after a data access exception! + session.setFlushMode(FlushMode.MANUAL) + throw e + } + } + /** + * Performs automatic association retrieval + * @param entity The domain class to retrieve associations for + * @param target The target object + */ + @SuppressWarnings('unchecked') + private void autoRetrieveAssociations(Datastore datastore, PersistentEntity entity, Object target) { + EntityReflector reflector = datastore.mappingContext.getEntityReflector(entity) + IHibernateTemplate t = this.hibernateTemplate + for (PersistentProperty prop in entity.associations) { + if (prop instanceof ToOne && !(prop instanceof Embedded)) { + ToOne toOne = (ToOne) prop + + def propertyName = prop.name + def propValue = reflector.getProperty(target, propertyName) + if (propValue == null || t.contains(propValue)) { + continue + } + + PersistentEntity otherSide = toOne.associatedEntity + if (otherSide == null) { + continue + } + + def identity = otherSide.identity + if (identity == null) { + continue + } + + def otherSideReflector = datastore.mappingContext.getEntityReflector(otherSide) + try { + def id = (Serializable) otherSideReflector.getProperty(propValue, identity.name) + if (id) { + final Object associatedInstance = t.get(prop.type, id) + if (associatedInstance) { + reflector.setProperty(target, propertyName, associatedInstance) + } + } + } + catch (InvalidPropertyException ipe) { + // property is not accessable + } + } + + } + } + + /** + * Checks whether validation should be performed + * @return true if the domain class should be validated + * @param arguments The arguments to the validate method + * @param domainClass The domain class + */ + private boolean shouldValidate(Map arguments, PersistentEntity entity) { + if (!entity) { + return false + } + + if (arguments?.containsKey(ARGUMENT_VALIDATE)) { + return ClassUtils.getBooleanFromMap(ARGUMENT_VALIDATE, arguments) + } + return true + } + + private boolean shouldInsert(Map arguments) { + ClassUtils.getBooleanFromMap(ARGUMENT_INSERT, arguments) + } + + private boolean shouldMerge(Map arguments) { + ClassUtils.getBooleanFromMap(ARGUMENT_MERGE, arguments) + } + + protected boolean shouldFlush(Map map) { + if (map?.containsKey(ARGUMENT_FLUSH)) { + return ClassUtils.getBooleanFromMap(ARGUMENT_FLUSH, map) + } + return autoFlush + } + + protected boolean shouldFail(Map map) { + if (map?.containsKey(ARGUMENT_FAIL_ON_ERROR)) { + return ClassUtils.getBooleanFromMap(ARGUMENT_FAIL_ON_ERROR, map) + } + return failOnError + } + + /** + * Sets the flush mode to manual. which ensures that the database changes are not persisted to the database + * if a validation error occurs. If save() is called again and validation passes the code will check if there + * is a manual flush mode and flush manually if necessary + * + * @param domainClass The domain class + * @param target The target object that failed validation + * @param errors The Errors instance @return This method will return null signaling a validation failure + */ + protected Object handleValidationError(PersistentEntity entity, final Object target, Errors errors) { + // if a validation error occurs set the object to read-only to prevent a flush + setObjectToReadOnly(target) + if (entity) { + for (Association association in entity.associations) { + if (association instanceof ToOne && !association instanceof Embedded) { + if (proxyHandler.isInitialized(target, association.name)) { + def bean = new BeanWrapperImpl(target) + def propertyValue = bean.getPropertyValue(association.name) + if (propertyValue != null) { + setObjectToReadOnly(propertyValue) + } + } + } + } + } + setErrorsOnInstance(target, errors) + return null + } + + /** + * Sets the target object to read-only using the given SessionFactory instance. This + * avoids Hibernate performing any dirty checking on the object + * + * + * @param target The target object + * @param sessionFactory The SessionFactory instance + */ + void setObjectToReadOnly(Object target) { + hibernateTemplate.execute { Session session -> + if (session.contains(target) && proxyHandler.isInitialized(target)) { + target = proxyHandler.unwrap(target) + session.setReadOnly(target, true) + session.flushMode = FlushMode.MANUAL + } + } + } + /** + * Sets the target object to read-write, allowing Hibernate to dirty check it and auto-flush changes. + * + * @see #setObjectToReadOnly(Object) + * + * @param target The target object + * @param sessionFactory The SessionFactory instance + */ + abstract void setObjectToReadWrite(Object target) + + /** + * Associates the Errors object on the instance + * + * @param target The target instance + * @param errors The Errors object + */ + @CompileDynamic + protected void setErrorsOnInstance(Object target, Errors errors) { + if (target instanceof GormValidateable) { + ((GormValidateable) target).setErrors(errors) + } + else { + target."$GormProperties.ERRORS" = errors + } + } + + /** + * Called by org.grails.orm.hibernate.metaclass.SavePersistentMethod's performInsert + * to set a ThreadLocal variable that determines the value for getAssumedUnsaved(). + */ + static void markInsertActive() { + insertActiveThreadLocal.set(Boolean.TRUE) + } + + /** + * Clears the ThreadLocal variable set by markInsertActive(). + */ + static void resetInsertActive() { + insertActiveThreadLocal.remove() + } + + /** + * Increments the entities version number in order to force an update + * @param target The target entity + */ + @CompileDynamic + protected void incrementVersion(Object target) { + if (target.hasProperty(GormProperties.VERSION)) { + Object version = target."${GormProperties.VERSION}" + if (version instanceof Long) { + target."${GormProperties.VERSION}" = ++((Long) version) + } + } + } + + SessionFactory getSessionFactory() { + return this.sessionFactory + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormStaticApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormStaticApi.groovy new file mode 100644 index 00000000000..8b478961a10 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormStaticApi.groovy @@ -0,0 +1,903 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery +import jakarta.persistence.criteria.Expression +import jakarta.persistence.criteria.Root + +import org.hibernate.Criteria +import org.hibernate.FlushMode +import org.hibernate.Session +import org.hibernate.criterion.Example +import org.hibernate.criterion.Restrictions +import org.hibernate.jpa.QueryHints +import org.hibernate.query.NativeQuery +import org.hibernate.query.Query +import org.hibernate.transform.DistinctRootEntityResultTransformer + +import org.springframework.core.convert.ConversionService +import org.springframework.transaction.PlatformTransactionManager + +import org.grails.datastore.gorm.GormStaticApi +import org.grails.datastore.gorm.finders.DynamicFinder +import org.grails.datastore.gorm.finders.FinderMethod +import org.grails.datastore.mapping.proxy.ProxyHandler +import org.grails.datastore.mapping.reflect.ClassUtils +import org.grails.orm.hibernate.cfg.AbstractGrailsDomainBinder +import org.grails.orm.hibernate.cfg.CompositeIdentity +import org.grails.orm.hibernate.exceptions.GrailsQueryException +import org.grails.orm.hibernate.query.GrailsHibernateQueryUtils +import org.grails.orm.hibernate.query.HibernateHqlQuery +import org.grails.orm.hibernate.support.HibernateRuntimeUtils + +/** + * Abstract implementation of the Hibernate static API for GORM, providing String-based method implementations + * + * @author Graeme Rocher + * @since 4.0 + */ +@CompileStatic +abstract class AbstractHibernateGormStaticApi extends GormStaticApi { + + protected ProxyHandler proxyHandler + protected GrailsHibernateTemplate hibernateTemplate + protected ConversionService conversionService + protected final HibernateSession hibernateSession + + AbstractHibernateGormStaticApi( + Class persistentClass, + HibernateDatastore datastore, + List finders) { + this(persistentClass, datastore, finders, null) + } + + AbstractHibernateGormStaticApi( + Class persistentClass, + HibernateDatastore datastore, + List finders, + PlatformTransactionManager transactionManager) { + super(persistentClass, datastore, finders, transactionManager) + this.hibernateTemplate = new GrailsHibernateTemplate(datastore.getSessionFactory(), datastore) + this.conversionService = datastore.mappingContext.conversionService + this.proxyHandler = datastore.mappingContext.proxyHandler + this.hibernateSession = new HibernateSession( + (HibernateDatastore) datastore, + hibernateTemplate.getSessionFactory(), + hibernateTemplate.getFlushMode() + ) + } + + IHibernateTemplate getHibernateTemplate() { + return hibernateTemplate + } + + @Override + T withNewSession(Closure callable) { + AbstractHibernateDatastore hibernateDatastore = (AbstractHibernateDatastore) datastore + hibernateDatastore.withNewSession(callable) + } + + @Override + def T withSession(Closure callable) { + AbstractHibernateDatastore hibernateDatastore = (AbstractHibernateDatastore) datastore + hibernateDatastore.withSession(callable) + } + + @Override + D get(Serializable id) { + if (id == null) { + return null + } + + id = convertIdentifier(id) + + if (id == null) { + return null + } + + if (persistentEntity.isMultiTenant()) { + // for multi-tenant entities we process get(..) via a query + + (D) hibernateTemplate.execute({ Session session -> + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder() + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(persistentEntity.javaClass) + Root queryRoot = criteriaQuery.from(persistentEntity.javaClass) + criteriaQuery = criteriaQuery.where( + //TODO: Remove explicit type cast once GROOVY-9460 + criteriaBuilder.equal((Expression) queryRoot.get(persistentEntity.identity.name), id) + ) + Query criteria = session.createQuery(criteriaQuery) + HibernateHqlQuery hibernateHqlQuery = new HibernateHqlQuery( + hibernateSession, persistentEntity, criteria) + return proxyHandler.unwrap(hibernateHqlQuery.singleResult()) + }) + } + else { + // for non multi-tenant entities we process get(..) via the second level cache + return (D) proxyHandler.unwrap( + hibernateTemplate.get(persistentEntity.javaClass, id) + ) + } + + } + + @Override + D read(Serializable id) { + if (id == null) { + return null + } + id = convertIdentifier(id) + + if (id == null) { + return null + } + + (D) hibernateTemplate.execute({ Session session -> + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder() + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(persistentEntity.javaClass) + + Root queryRoot = criteriaQuery.from(persistentEntity.javaClass) + criteriaQuery = criteriaQuery.where( + //TODO: Remove explicit type cast once GROOVY-9460 + criteriaBuilder.equal((Expression) queryRoot.get(persistentEntity.identity.name), id) + ) + Query criteria = session.createQuery(criteriaQuery) + .setHint(QueryHints.HINT_READONLY, true) + HibernateHqlQuery hibernateHqlQuery = new HibernateHqlQuery( + hibernateSession, persistentEntity, criteria) + return proxyHandler.unwrap(hibernateHqlQuery.singleResult()) + + }) + } + + @Override + D load(Serializable id) { + id = convertIdentifier(id) + if (id != null) { + return (D) hibernateTemplate.load((Class)persistentClass, id) + } + else { + return null + } + } + + @Override + List getAll() { + (List) hibernateTemplate.execute({ Session session -> + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder() + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(persistentEntity.javaClass) + Query criteria = session.createQuery(criteriaQuery) + HibernateHqlQuery hibernateHqlQuery = new HibernateHqlQuery( + hibernateSession, persistentEntity, criteria) + return hibernateHqlQuery.list() + }) + } + + @Override + Integer count() { + (Integer) hibernateTemplate.execute({ Session session -> + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder() + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(persistentEntity.javaClass) + criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(persistentEntity.javaClass))) + Query criteria = session.createQuery(criteriaQuery) + HibernateHqlQuery hibernateHqlQuery = new HibernateHqlQuery( + hibernateSession, persistentEntity, criteria) { + @Override + protected void flushBeforeQuery() { + // no-op + } + } + hibernateTemplate.applySettings(criteria) + def result = hibernateHqlQuery.singleResult() + Number num = result == null ? 0 : (Number)result + return num + }) + } + + /** + * Fire a post query event + * + * @param session The session + * @param criteria The criteria + * @param result The result + */ + protected abstract void firePostQueryEvent(Session session, Criteria criteria, Object result) + /** + * Fire a pre query event + * + * @param session The session + * @param criteria The criteria + * @return True if the query should be cancelled + */ + protected abstract void firePreQueryEvent(Session session, Criteria criteria) + + @Override + boolean exists(Serializable id) { + id = convertIdentifier(id) + hibernateTemplate.execute { Session session -> + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder() + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(persistentEntity.javaClass) + Root queryRoot = criteriaQuery.from(persistentEntity.javaClass) + def idProp = queryRoot.get(persistentEntity.identity.name) + criteriaQuery = criteriaQuery.where( + //TODO: Remove explicit type cast once GROOVY-9460 + criteriaBuilder.equal((Expression) idProp, id) + ) + criteriaQuery.select(criteriaBuilder.count(queryRoot)) + Query criteria = session.createQuery(criteriaQuery) + HibernateHqlQuery hibernateHqlQuery = new HibernateHqlQuery( + hibernateSession, persistentEntity, criteria) + + hibernateTemplate.applySettings(criteria) + Boolean result = hibernateHqlQuery.singleResult() + return result + } + } + + D first(Map m) { + def entityMapping = AbstractGrailsDomainBinder.getMapping(persistentEntity.javaClass) + if (entityMapping?.identity instanceof CompositeIdentity) { + throw new UnsupportedOperationException('The first() method is not supported for domain classes that have composite keys.') + } + super.first(m) + } + + D last(Map m) { + def entityMapping = AbstractGrailsDomainBinder.getMapping(persistentEntity.javaClass) + if (entityMapping?.identity instanceof CompositeIdentity) { + throw new UnsupportedOperationException('The last() method is not supported for domain classes that have composite keys.') + } + super.last(m) + } + + /** + * Implements the 'find(String' method to use HQL queries with named arguments + * + * @param query The query + * @param queryNamedArgs The named arguments + * @param args Any additional query arguments + * @return A result or null if no result found + */ + @Override + D find(CharSequence query, Map queryNamedArgs, Map args) { + queryNamedArgs = new LinkedHashMap(queryNamedArgs) + args = new LinkedHashMap(args) + if (query instanceof GString) { + query = buildNamedParameterQueryFromGString((GString) query, queryNamedArgs) + } + + String queryString = query.toString() + query = normalizeMultiLineQueryString(queryString) + + def template = hibernateTemplate + queryNamedArgs = new HashMap(queryNamedArgs) + return (D) template.execute { Session session -> + Query q = (Query) session.createQuery(queryString) + template.applySettings(q) + + populateQueryArguments(q, queryNamedArgs) + populateQueryArguments(q, args) + populateQueryWithNamedArguments(q, queryNamedArgs) + proxyHandler.unwrap(createHqlQuery(session, q).singleResult()) + } + } + + protected abstract HibernateHqlQuery createHqlQuery(Session session, Query q) + + @Override + D find(CharSequence query, Collection params, Map args) { + if (query instanceof GString) { + throw new GrailsQueryException("Unsafe query [$query]. GORM cannot automatically escape a GString value when combined with ordinal parameters, so this query is potentially vulnerable to HQL injection attacks. Please embed the parameters within the GString so they can be safely escaped.") + } + + String queryString = query.toString() + queryString = normalizeMultiLineQueryString(queryString) + + args = new HashMap(args) + def template = hibernateTemplate + return (D) template.execute { Session session -> + Query q = (Query) session.createQuery(queryString) + template.applySettings(q) + + params.eachWithIndex { val, int i -> + if (val instanceof CharSequence) { + q.setParameter(i, val.toString()) + } + else { + q.setParameter(i, val) + } + } + populateQueryArguments(q, args) + proxyHandler.unwrap(createHqlQuery(session, q).singleResult()) + } + } + + @Override + List findAll(CharSequence query, Map params, Map args) { + params = new LinkedHashMap(params) + args = new LinkedHashMap(args) + if (query instanceof GString) { + query = buildNamedParameterQueryFromGString((GString) query, params) + } + + String queryString = query.toString() + queryString = normalizeMultiLineQueryString(queryString) + + def template = hibernateTemplate + return (List) template.execute { Session session -> + Query q = (Query) session.createQuery(queryString) + template.applySettings(q) + + populateQueryArguments(q, params) + populateQueryArguments(q, args) + populateQueryWithNamedArguments(q, params) + + createHqlQuery(session, q).list() + } + } + + @CompileDynamic // required for Hibernate 5.2 compatibility + def D findWithSql(CharSequence sql, Map args = Collections.emptyMap()) { + IHibernateTemplate template = hibernateTemplate + return (D) template.execute { Session session -> + + List params = [] + if (sql instanceof GString) { + sql = buildOrdinalParameterQueryFromGString((GString)sql, params) + } + + NativeQuery q = (NativeQuery)session.createNativeQuery(sql.toString()) + + template.applySettings(q) + + params.eachWithIndex { val, int i -> + i++ + if (val instanceof CharSequence) { + q.setParameter(i, val.toString()) + } + else { + q.setParameter(i, val) + } + } + q.addEntity(persistentClass) + populateQueryArguments(q, args) + q.setMaxResults(1) + def results = createHqlQuery(session, q).list() + if (results.isEmpty()) { + return null + } + else { + return results.get(0) + } + } + } + + /** + * Finds all results for this entity for the given SQL query + * + * @param sql The SQL query + * @param args The arguments + * @return All entities matching the SQL query + */ + @CompileDynamic // required for Hibernate 5.2 compatibility + List findAllWithSql(CharSequence sql, Map args = Collections.emptyMap()) { + IHibernateTemplate template = hibernateTemplate + return (List) template.execute { Session session -> + + List params = [] + if (sql instanceof GString) { + sql = buildOrdinalParameterQueryFromGString((GString)sql, params) + } + + NativeQuery q = (NativeQuery)session.createNativeQuery(sql.toString()) + + template.applySettings(q) + + params.eachWithIndex { val, int i -> + i++ + if (val instanceof CharSequence) { + q.setParameter(i, val.toString()) + } + else { + q.setParameter(i, val) + } + } + q.addEntity(persistentClass) + populateQueryArguments(q, args) + return createHqlQuery(session, q).list() + } + } + + @Override + List findAll(CharSequence query) { + if (query instanceof GString) { + Map params = [:] + String hql = buildNamedParameterQueryFromGString((GString) query, params) + return findAll(hql, params, Collections.emptyMap()) + } + else { + return super.findAll(query) + } + } + + @Override + List executeQuery(CharSequence query) { + if (query instanceof GString) { + Map params = [:] + String hql = buildNamedParameterQueryFromGString((GString) query, params) + return executeQuery(hql, params, Collections.emptyMap()) + } + else { + return super.executeQuery(query) + } + } + + @Override + Integer executeUpdate(CharSequence query) { + if (query instanceof GString) { + Map params = [:] + String hql = buildNamedParameterQueryFromGString((GString) query, params) + return executeUpdate(hql, params, Collections.emptyMap()) + } + else { + return super.executeUpdate(query) + } + } + + @Override + D find(CharSequence query) { + if (query instanceof GString) { + Map params = [:] + String hql = buildNamedParameterQueryFromGString((GString) query, params) + return find(hql, params, Collections.emptyMap()) + } + else { + return (D) super.find(query) + } + } + + @Override + D find(CharSequence query, Map params) { + if (query instanceof GString) { + Map newParams = new LinkedHashMap(params) + String hql = buildNamedParameterQueryFromGString((GString) query, newParams) + return find(hql, newParams, newParams) + } + else { + return (D) super.find(query, params) + } + } + + @Override + List findAll(CharSequence query, Map params) { + if (query instanceof GString) { + Map newParams = new LinkedHashMap(params) + String hql = buildNamedParameterQueryFromGString((GString) query, newParams) + return findAll(hql, newParams, newParams) + } + else { + return super.findAll(query, params) + } + } + + @Override + List executeQuery(CharSequence query, Map args) { + if (query instanceof GString) { + Map newParams = new LinkedHashMap(args) + String hql = buildNamedParameterQueryFromGString((GString) query, newParams) + return executeQuery(hql, newParams, newParams) + } + else { + return super.executeQuery(query, args) + } + } + + @Override + Integer executeUpdate(CharSequence query, Map args) { + if (query instanceof GString) { + Map newParams = new LinkedHashMap(args) + String hql = buildNamedParameterQueryFromGString((GString) query, newParams) + return executeUpdate(hql, newParams, newParams) + } + else { + return super.executeUpdate(query, args) + } + } + + @Override + List findAll(CharSequence query, Collection params, Map args) { + if (query instanceof GString) { + throw new GrailsQueryException("Unsafe query [$query]. GORM cannot automatically escape a GString value when combined with ordinal parameters, so this query is potentially vulnerable to HQL injection attacks. Please embed the parameters within the GString so they can be safely escaped.") + } + + String queryString = query.toString() + queryString = normalizeMultiLineQueryString(queryString) + + args = new HashMap(args) + + def template = hibernateTemplate + return (List) template.execute { Session session -> + Query q = (Query) session.createQuery(queryString) + template.applySettings(q) + + params.eachWithIndex { val, int i -> + if (val instanceof CharSequence) { + q.setParameter(i, val.toString()) + } + else { + q.setParameter(i, val) + } + } + populateQueryArguments(q, args) + createHqlQuery(session, q).list() + } + } + + @Override + D find(D exampleObject, Map args) { + def template = hibernateTemplate + return (D) template.execute { Session session -> + Example example = Example.create(exampleObject).ignoreCase() + + Criteria crit = session.createCriteria(persistentEntity.javaClass) + hibernateTemplate.applySettings(crit) + crit.add(example) + GrailsHibernateQueryUtils.populateArgumentsForCriteria(persistentEntity, crit, args, datastore.mappingContext.conversionService, true) + crit.maxResults = 1 + firePreQueryEvent(session, crit) + List results = crit.list() + firePostQueryEvent(session, crit, results) + if (results) { + return proxyHandler.unwrap(results.get(0)) + } + } + } + + @Override + List findAll(D exampleObject, Map args) { + def template = hibernateTemplate + return (List) template.execute { Session session -> + Example example = Example.create(exampleObject).ignoreCase() + + Criteria crit = session.createCriteria(persistentEntity.javaClass) + hibernateTemplate.applySettings(crit) + crit.add(example) + GrailsHibernateQueryUtils.populateArgumentsForCriteria(persistentEntity, crit, args, datastore.mappingContext.conversionService, true) + firePreQueryEvent(session, crit) + List results = crit.list() + firePostQueryEvent(session, crit, results) + return results + } + } + + @Override + List findAllWhere(Map queryMap, Map args) { + if (!queryMap) return null + (List) hibernateTemplate.execute { Session session -> + Map processedQueryMap = [:] + queryMap.each { key, value -> processedQueryMap[key.toString()] = value } + Map queryArgs = filterQueryArgumentMap(processedQueryMap) + List nullNames = removeNullNames(queryArgs) + Criteria criteria = session.createCriteria(persistentClass) + hibernateTemplate.applySettings(criteria) + criteria.add(Restrictions.allEq(queryArgs)) + for (name in nullNames) { + criteria.add(Restrictions.isNull(name)) + } + criteria.setResultTransformer(DistinctRootEntityResultTransformer.INSTANCE) + + GrailsHibernateQueryUtils.populateArgumentsForCriteria(persistentEntity, criteria, args, datastore.mappingContext.conversionService, true) + firePreQueryEvent(session, criteria) + List results = criteria.list() + firePostQueryEvent(session, criteria, results) + return results + } + } + + @Override + List executeQuery(CharSequence query, Map params, Map args) { + def template = hibernateTemplate + args = new HashMap(args) + params = new HashMap(params) + + if (query instanceof GString) { + query = buildNamedParameterQueryFromGString((GString) query, params) + } + + return (List) template.execute { Session session -> + Query q = (Query) session.createQuery(query.toString()) + template.applySettings(q) + + populateQueryArguments(q, params) + populateQueryArguments(q, args) + populateQueryWithNamedArguments(q, params) + + createHqlQuery(session, q).list() + } + } + + @Override + List executeQuery(CharSequence query, Collection params, Map args) { + if (query instanceof GString) { + throw new GrailsQueryException("Unsafe query [$query]. GORM cannot automatically escape a GString value when combined with ordinal parameters, so this query is potentially vulnerable to HQL injection attacks. Please embed the parameters within the GString so they can be safely escaped.") + } + + def template = hibernateTemplate + args = new HashMap(args) + + return (List) template.execute { Session session -> + Query q = (Query) session.createQuery(query.toString()) + template.applySettings(q) + + params.eachWithIndex { val, int i -> + if (val instanceof CharSequence) { + q.setParameter(i, val.toString()) + } + else { + q.setParameter(i, val) + } + } + populateQueryArguments(q, args) + createHqlQuery(session, q).list() + } + } + + @Override + D findWhere(Map queryMap, Map args) { + if (!queryMap) return null + (D) hibernateTemplate.execute { Session session -> + Map processedQueryMap = [:] + queryMap.each { key, value -> processedQueryMap[key.toString()] = value } + Map queryArgs = filterQueryArgumentMap(processedQueryMap) + List nullNames = removeNullNames(queryArgs) + Criteria criteria = session.createCriteria(persistentClass) + hibernateTemplate.applySettings(criteria) + criteria.add(Restrictions.allEq(queryArgs)) + for (name in nullNames) { + criteria.add(Restrictions.isNull(name)) + } + criteria.setMaxResults(1) + GrailsHibernateQueryUtils.populateArgumentsForCriteria(persistentEntity, criteria, args, datastore.mappingContext.conversionService, true) + firePreQueryEvent(session, criteria) + Object result = criteria.uniqueResult() + firePostQueryEvent(session, criteria, result) + return proxyHandler.unwrap(result) + } + } + + List getAll(List ids) { + getAllInternal(ids) + } + + List getAll(Long... ids) { + getAllInternal(ids as List) + } + + @Override + List getAll(Serializable... ids) { + getAllInternal(ids as List) + } + + @CompileDynamic + private List getAllInternal(List ids) { + if (!ids) return [] + + (List) hibernateTemplate.execute { Session session -> + def identityType = persistentEntity.identity.type + ids = ids.collect { HibernateRuntimeUtils.convertValueToType((Serializable)it, identityType, conversionService) } + def criteria = session.createCriteria(persistentClass) + hibernateTemplate.applySettings(criteria) + def identityName = persistentEntity.identity.name + criteria.add(Restrictions.'in'(identityName, ids)) + firePreQueryEvent(session, criteria) + List results = criteria.list() + firePostQueryEvent(session, criteria, results) + def idsMap = [:] + for (object in results) { + idsMap[object[identityName]] = object + } + results.clear() + for (id in ids) { + results << idsMap[id] + } + results + } + } + + protected Map filterQueryArgumentMap(Map query) { + def queryArgs = [:] + for (entry in query.entrySet()) { + if (entry.value instanceof CharSequence) { + queryArgs[entry.key] = entry.value.toString() + } + else { + queryArgs[entry.key] = entry.value + } + } + return queryArgs + } + + /** + * Processes a query converting GString expressions into parameters + * + * @param query The query + * @param params The parameters + * @return The final String + */ + protected String buildOrdinalParameterQueryFromGString(GString query, List params) { + StringBuilder sqlString = new StringBuilder() + int i = 0 + Object[] values = query.values + def strings = query.getStrings() + for (str in strings) { + sqlString.append(str) + if (i < values.length) { + sqlString.append('?') + params.add(values[i++]) + } + } + return sqlString.toString() + } + + /** + * Processes a query converting GString expressions into parameters + * + * @param query The query + * @param params The parameters + * @return The final String + */ + protected String buildNamedParameterQueryFromGString(GString query, Map params) { + StringBuilder sqlString = new StringBuilder() + int i = 0 + Object[] values = query.values + def strings = query.getStrings() + for (str in strings) { + sqlString.append(str) + if (i < values.length) { + String parameterName = "p$i" + sqlString.append(':').append(parameterName) + params.put(parameterName, values[i++]) + } + } + return sqlString.toString() + } + + protected List removeNullNames(Map query) { + List nullNames = [] + Set allNames = new HashSet<>(query.keySet() as Set) + for (String name in allNames) { + if (query[name] == null) { + query.remove(name) + nullNames << name + } + } + nullNames + } + + protected Serializable convertIdentifier(Serializable id) { + def identity = persistentEntity.identity + if (identity != null) { + ConversionService conversionService = persistentEntity.mappingContext.conversionService + if (id != null) { + Class identityType = identity.type + Class idInstanceType = id.getClass() + if (identityType.isAssignableFrom(idInstanceType)) { + return id + } + else if (conversionService.canConvert(idInstanceType, identityType)) { + try { + return (Serializable) conversionService.convert(id, identityType) + } catch (Throwable e) { + // unconvertable id, return null + return null + } + } + else { + // unconvertable id, return null + return null + } + } + } + return id + } + + protected void populateQueryWithNamedArguments(Query q, Map queryNamedArgs) { + + if (queryNamedArgs) { + for (Map.Entry entry in queryNamedArgs.entrySet()) { + def key = entry.key + if (!(key instanceof CharSequence)) { + throw new GrailsQueryException("Named parameter's name must be String: $queryNamedArgs") + } + String stringKey = key.toString() + def value = entry.value + + if (value == null) { + q.setParameter(stringKey, null) + } else if (value instanceof CharSequence) { + q.setParameter(stringKey, value.toString()) + } else if (List.isAssignableFrom(value.getClass())) { + q.setParameterList(stringKey, (List) value) + } else if (Set.isAssignableFrom(value.getClass())) { + q.setParameterList(stringKey, (Set) value) + } else if (value.getClass().isArray()) { + q.setParameterList(stringKey, (Object[]) value) + } else { + q.setParameter(stringKey, value) + } + } + } + } + + protected Integer intValue(Map args, String key) { + def value = args.get(key) + if (value) { + return conversionService.convert(value, Integer) + } + return null + } + + protected void populateQueryArguments(Query q, Map args) { + Integer max = intValue(args, DynamicFinder.ARGUMENT_MAX) + args.remove(DynamicFinder.ARGUMENT_MAX) + Integer offset = intValue(args, DynamicFinder.ARGUMENT_OFFSET) + args.remove(DynamicFinder.ARGUMENT_OFFSET) + + // + if (max != null) { + q.maxResults = max + } + if (offset != null) { + q.firstResult = offset + } + + if (args.containsKey(DynamicFinder.ARGUMENT_CACHE)) { + q.cacheable = ClassUtils.getBooleanFromMap(DynamicFinder.ARGUMENT_CACHE, args) + } + if (args.containsKey(DynamicFinder.ARGUMENT_FETCH_SIZE)) { + Integer fetchSizeParam = conversionService.convert(args.remove(DynamicFinder.ARGUMENT_FETCH_SIZE), Integer) + q.setFetchSize(fetchSizeParam.intValue()) + } + if (args.containsKey(DynamicFinder.ARGUMENT_TIMEOUT)) { + Integer timeoutParam = conversionService.convert(args.remove(DynamicFinder.ARGUMENT_TIMEOUT), Integer) + q.setTimeout(timeoutParam.intValue()) + } + if (args.containsKey(DynamicFinder.ARGUMENT_READ_ONLY)) { + q.setReadOnly((Boolean) args.remove(DynamicFinder.ARGUMENT_READ_ONLY)) + } + if (args.containsKey(DynamicFinder.ARGUMENT_FLUSH_MODE)) { + q.setHibernateFlushMode((FlushMode) args.remove(DynamicFinder.ARGUMENT_FLUSH_MODE)) + } + + args.remove(DynamicFinder.ARGUMENT_CACHE) + } + + private String normalizeMultiLineQueryString(String query) { + if (query.indexOf('\n') != -1) + return query.trim().replace('\n', ' ') + return query + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormValidationApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormValidationApi.groovy new file mode 100644 index 00000000000..bf21ffb7c61 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormValidationApi.groovy @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import groovy.transform.CompileStatic + +import org.hibernate.Session + +import org.springframework.validation.Errors +import org.springframework.validation.FieldError +import org.springframework.validation.ObjectError +import org.springframework.validation.Validator + +import org.grails.datastore.gorm.GormValidationApi +import org.grails.datastore.gorm.validation.CascadingValidator +import org.grails.datastore.mapping.engine.event.ValidationEvent +import org.grails.datastore.mapping.reflect.ClassUtils +import org.grails.datastore.mapping.validation.ValidationErrors +import org.grails.orm.hibernate.support.HibernateRuntimeUtils + +@CompileStatic +abstract class AbstractHibernateGormValidationApi extends GormValidationApi { + + public static final String ARGUMENT_DEEP_VALIDATE = 'deepValidate' + private static final String ARGUMENT_EVICT = 'evict' + + protected ClassLoader classLoader + protected AbstractHibernateDatastore datastore + protected IHibernateTemplate hibernateTemplate + + protected AbstractHibernateGormValidationApi(Class persistentClass, AbstractHibernateDatastore datastore, ClassLoader classLoader) { + super(persistentClass, datastore) + this.classLoader = classLoader + this.datastore = datastore + } + + @Override + boolean validate(D instance, Map arguments = Collections.emptyMap()) { + validate(instance, null, arguments) + } + + boolean validate(D instance, List validatedFieldsList, Map arguments = Collections.emptyMap()) { + Errors errors = setupErrorsProperty(instance) + + Validator validator = getValidator() + if (validator == null) return true + + Boolean valid = Boolean.TRUE + // should evict? + boolean evict = false + boolean deepValidate = true + Set validatedFields = null + if (validatedFieldsList != null) { + validatedFields = new HashSet(validatedFieldsList) + } + + if (arguments?.containsKey(ARGUMENT_DEEP_VALIDATE)) { + deepValidate = ClassUtils.getBooleanFromMap(ARGUMENT_DEEP_VALIDATE, arguments) + } + + evict = ClassUtils.getBooleanFromMap(ARGUMENT_EVICT, arguments) + + fireEvent(instance, validatedFieldsList) + + hibernateTemplate.execute { Session session -> + + def previous = readPreviousFlushMode(session) + applyManualFlush(session) + try { + if (validator instanceof CascadingValidator) { + ((CascadingValidator) validator).validate(instance, errors, deepValidate) + } else if (validator instanceof grails.gorm.validation.CascadingValidator) { + ((grails.gorm.validation.CascadingValidator) validator).validate(instance, errors, deepValidate) + } else { + validator.validate(instance, errors) + } + } finally { + if (!errors.hasErrors()) { + restoreFlushMode(session, previous) + } + } + + } + + int oldErrorCount = errors.errorCount + errors = filterErrors(errors, validatedFields, instance) + + if (errors.hasErrors()) { + valid = Boolean.FALSE + if (evict) { + // if an boolean argument 'true' is passed to the method + // and validation fails then the object will be evicted + // from the session, ensuring it is not saved later when + // flush is called + if (hibernateTemplate.contains(instance)) { + hibernateTemplate.evict(instance) + } + } + } + + // If the errors have been filtered, update the 'errors' object attached to the target. + if (errors.errorCount != oldErrorCount) { + setErrors(instance, errors) + } + + return valid + } + + abstract void restoreFlushMode(Session session, Object previousFlushMode) + + abstract Object readPreviousFlushMode(Session session) + + abstract applyManualFlush(Session session) + + private void fireEvent(Object target, List validatedFieldsList) { + ValidationEvent event = new ValidationEvent(datastore, target) + event.setValidatedFields(validatedFieldsList) + datastore.getApplicationEventPublisher().publishEvent(event) + } + + @SuppressWarnings('rawtypes') + private Errors filterErrors(Errors errors, Set validatedFields, Object target) { + if (validatedFields == null) return errors + + ValidationErrors result = new ValidationErrors(target) + + final List allErrors = errors.getAllErrors() + for (Object allError : allErrors) { + ObjectError error = (ObjectError) allError + + if (error instanceof FieldError) { + FieldError fieldError = (FieldError) error + if (!validatedFields.contains(fieldError.getField())) continue + } + + result.addError(error) + } + + return result + } + + /** + * Initializes the Errors property on target. The target will be assigned a new + * Errors property. If the target contains any binding errors, those binding + * errors will be copied in to the new Errors property. + * + * @param target object to initialize + * @return the new Errors object + */ + protected Errors setupErrorsProperty(Object target) { + HibernateRuntimeUtils.setupErrorsProperty(target) + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateSession.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateSession.java new file mode 100644 index 00000000000..17843905059 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateSession.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import jakarta.persistence.FlushModeType; + +import org.hibernate.LockMode; +import org.hibernate.SessionFactory; + +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import org.grails.datastore.mapping.core.AbstractAttributeStoringSession; +import org.grails.datastore.mapping.core.Datastore; +import org.grails.datastore.mapping.engine.Persister; +import org.grails.datastore.mapping.model.MappingContext; +import org.grails.datastore.mapping.query.api.QueryAliasAwareSession; +import org.grails.datastore.mapping.transactions.Transaction; + +/** + * Session implementation that wraps a Hibernate {@link org.hibernate.Session}. + * + * @author Graeme Rocher + * @since 1.0 + */ +@SuppressWarnings("rawtypes") +public abstract class AbstractHibernateSession extends AbstractAttributeStoringSession implements QueryAliasAwareSession { + + protected AbstractHibernateDatastore datastore; + protected boolean connected = true; + protected IHibernateTemplate hibernateTemplate; + + protected AbstractHibernateSession(AbstractHibernateDatastore hibernateDatastore, SessionFactory sessionFactory) { + datastore = hibernateDatastore; + } + + @Override + public boolean isSchemaless() { + return false; + } + + public Serializable insert(Object o) { + return persist(o); + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public void disconnect() { + connected = false; // don't actually do any disconnection here. This will be handled by OSVI + } + + public Transaction beginTransaction() { + throw new UnsupportedOperationException("Use HibernatePlatformTransactionManager instead"); + } + + @Override + public Transaction beginTransaction(TransactionDefinition definition) { + throw new UnsupportedOperationException("Use HibernatePlatformTransactionManager instead"); + } + + public MappingContext getMappingContext() { + return getDatastore().getMappingContext(); + } + + public Serializable persist(Object o) { + return hibernateTemplate.save(o); + } + + public void refresh(Object o) { + hibernateTemplate.refresh(o); + } + + public void attach(Object o) { + hibernateTemplate.lock(o, LockMode.NONE); + } + + public void flush() { + hibernateTemplate.flush(); + } + + public void clear() { + hibernateTemplate.clear(); + } + + public void clear(Object o) { + hibernateTemplate.evict(o); + } + + public boolean contains(Object o) { + return hibernateTemplate.contains(o); + } + + public void lock(Object o) { + hibernateTemplate.lock(o, LockMode.PESSIMISTIC_WRITE); + } + + public void unlock(Object o) { + // do nothing + } + + public List persist(Iterable objects) { + List identifiers = new ArrayList<>(); + for (Object object : objects) { + identifiers.add(hibernateTemplate.save(object)); + } + return identifiers; + } + + public T retrieve(Class type, Serializable key) { + return hibernateTemplate.get(type, key); + } + + public T proxy(Class type, Serializable key) { + return hibernateTemplate.load(type, key); + } + + public T lock(Class type, Serializable key) { + return hibernateTemplate.get(type, key, LockMode.PESSIMISTIC_WRITE); + } + + public void delete(Iterable objects) { + Collection list = getIterableAsCollection(objects); + hibernateTemplate.deleteAll(list); + } + + @SuppressWarnings("unchecked") + protected Collection getIterableAsCollection(Iterable objects) { + Collection list; + if (objects instanceof Collection) { + list = (Collection) objects; + } + else { + list = new ArrayList(); + for (Object object : objects) { + list.add(object); + } + } + return list; + } + + public void delete(Object obj) { + hibernateTemplate.delete(obj); + } + + public List retrieveAll(Class type, Serializable... keys) { + return retrieveAll(type, Arrays.asList(keys)); + } + + public Persister getPersister(Object o) { + return null; + } + + public Transaction getTransaction() { + throw new UnsupportedOperationException("Use HibernatePlatformTransactionManager instead"); + } + + @Override + public boolean hasTransaction() { + Object resource = TransactionSynchronizationManager.getResource(hibernateTemplate.getSessionFactory()); + return resource != null; + } + + public Datastore getDatastore() { + return datastore; + } + + public boolean isDirty(Object o) { + // not used, Hibernate manages dirty checking itself + return true; + } + + public Object getNativeInterface() { + return hibernateTemplate; + } + + @Override + public void setSynchronizedWithTransaction(boolean synchronizedWithTransaction) { + // no-op + } + + public abstract FlushModeType getFlushMode(); +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/EventListenerIntegrator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/EventListenerIntegrator.java new file mode 100644 index 00000000000..749a3250b10 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/EventListenerIntegrator.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerGroup; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +public class EventListenerIntegrator implements Integrator { + + protected HibernateEventListeners hibernateEventListeners; + protected Map eventListeners; + + public EventListenerIntegrator(HibernateEventListeners hibernateEventListeners, Map eventListeners) { + this.hibernateEventListeners = hibernateEventListeners; + this.eventListeners = eventListeners; + } + + @SuppressWarnings("unchecked") + protected static final List> TYPES = Arrays.asList( + EventType.AUTO_FLUSH, + EventType.MERGE, + EventType.PERSIST, + EventType.PERSIST_ONFLUSH, + EventType.DELETE, + EventType.DIRTY_CHECK, + EventType.EVICT, + EventType.FLUSH, + EventType.FLUSH_ENTITY, + EventType.LOAD, + EventType.INIT_COLLECTION, + EventType.LOCK, + EventType.REFRESH, + EventType.REPLICATE, + EventType.SAVE_UPDATE, + EventType.SAVE, + EventType.UPDATE, + EventType.PRE_LOAD, + EventType.PRE_UPDATE, + EventType.PRE_DELETE, + EventType.PRE_INSERT, + EventType.PRE_COLLECTION_RECREATE, + EventType.PRE_COLLECTION_REMOVE, + EventType.PRE_COLLECTION_UPDATE, + EventType.POST_LOAD, + EventType.POST_UPDATE, + EventType.POST_DELETE, + EventType.POST_INSERT, + EventType.POST_COMMIT_UPDATE, + EventType.POST_COMMIT_DELETE, + EventType.POST_COMMIT_INSERT, + EventType.POST_COLLECTION_RECREATE, + EventType.POST_COLLECTION_REMOVE, + EventType.POST_COLLECTION_UPDATE); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { + + EventListenerRegistry listenerRegistry = serviceRegistry.getService(EventListenerRegistry.class); + + if (eventListeners != null) { + for (Map.Entry entry : eventListeners.entrySet()) { + EventType type = EventType.resolveEventTypeByName(entry.getKey()); + Object listenerObject = entry.getValue(); + if (listenerObject instanceof Collection) { + appendListeners(listenerRegistry, type, (Collection) listenerObject); + } + else if (listenerObject != null) { + appendListeners(listenerRegistry, type, Collections.singleton(listenerObject)); + } + } + } + + if (hibernateEventListeners != null && hibernateEventListeners.getListenerMap() != null) { + Map listenerMap = hibernateEventListeners.getListenerMap(); + for (EventType type : TYPES) { + appendListeners(listenerRegistry, type, listenerMap); + } + } + + } + + protected void appendListeners(EventListenerRegistry listenerRegistry, + EventType eventType, Collection listeners) { + + EventListenerGroup group = listenerRegistry.getEventListenerGroup(eventType); + for (T listener : listeners) { + if (listener != null) { + if (shouldOverrideListeners(eventType, listener)) { + // since ClosureEventTriggeringInterceptor extends DefaultSaveOrUpdateEventListener we want to override instead of append the listener here + // to avoid there being 2 implementations which would impact performance too + group.clear(); + group.appendListener(listener); + } + else { + group.appendListener(listener); + } + } + } + } + + private boolean shouldOverrideListeners(EventType eventType, Object listener) { + return (listener instanceof org.hibernate.event.internal.DefaultSaveOrUpdateEventListener) && + eventType.equals(EventType.SAVE_UPDATE); + } + + @SuppressWarnings("unchecked") + protected void appendListeners(final EventListenerRegistry listenerRegistry, + final EventType eventType, final Map listeners) { + + Object listener = listeners.get(eventType.eventName()); + if (listener != null) { + if (shouldOverrideListeners(eventType, listener)) { + // since ClosureEventTriggeringInterceptor extends DefaultSaveOrUpdateEventListener we want to override instead of append the listener here + // to avoid there being 2 implementations which would impact performance too + listenerRegistry.setListeners(eventType, (T) listener); + } + else { + listenerRegistry.appendListeners(eventType, (T) listener); + } + } + } + + public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { + // nothing to do + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java new file mode 100644 index 00000000000..1df7d1ad09a --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java @@ -0,0 +1,784 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.sql.DataSource; + +import groovy.lang.Closure; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; + +import jakarta.persistence.PersistenceException; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import org.hibernate.Criteria; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.event.spi.EventSource; +import org.hibernate.exception.GenericJDBCException; +import org.hibernate.query.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +import org.grails.orm.hibernate.support.hibernate5.SessionFactoryUtils; +import org.grails.orm.hibernate.support.hibernate5.SessionHolder; + +public class GrailsHibernateTemplate implements IHibernateTemplate { + + private static final Logger LOG = LoggerFactory.getLogger(GrailsHibernateTemplate.class); + + private boolean osivReadOnly; + private boolean passReadOnlyToHibernate = false; + protected boolean exposeNativeSession = true; + protected boolean cacheQueries = false; + + protected SessionFactory sessionFactory; + protected DataSource dataSource = null; + protected SQLExceptionTranslator jdbcExceptionTranslator; + protected int flushMode = FLUSH_AUTO; + private boolean applyFlushModeOnlyToNonExistingTransactions = false; + + public interface HibernateCallback { + T doInHibernate(Session session) throws HibernateException, SQLException; + } + + protected GrailsHibernateTemplate() { + // for testing + } + + public GrailsHibernateTemplate(SessionFactory sessionFactory) { + Assert.notNull(sessionFactory, "Property 'sessionFactory' is required"); + this.sessionFactory = sessionFactory; + + ConnectionProvider connectionProvider = ((SessionFactoryImplementor) sessionFactory).getServiceRegistry().getService(ConnectionProvider.class); + if (connectionProvider instanceof DatasourceConnectionProviderImpl) { + this.dataSource = ((DatasourceConnectionProviderImpl) connectionProvider).getDataSource(); + if (dataSource instanceof TransactionAwareDataSourceProxy) { + this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } + jdbcExceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource); + } + else { + // must be in unit test mode, setup default translator + SQLErrorCodeSQLExceptionTranslator sqlErrorCodeSQLExceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(); + sqlErrorCodeSQLExceptionTranslator.setDatabaseProductName("H2"); + jdbcExceptionTranslator = sqlErrorCodeSQLExceptionTranslator; + } + } + + public GrailsHibernateTemplate(SessionFactory sessionFactory, HibernateDatastore datastore) { + this(sessionFactory, datastore, datastore.getDefaultFlushMode()); + } + + public GrailsHibernateTemplate(SessionFactory sessionFactory, HibernateDatastore datastore, int defaultFlushMode) { + this(sessionFactory); + if (datastore != null) { + cacheQueries = datastore.isCacheQueries(); + this.osivReadOnly = datastore.isOsivReadOnly(); + this.passReadOnlyToHibernate = datastore.isPassReadOnlyToHibernate(); + } + this.flushMode = defaultFlushMode; + } + + @Override + public T execute(Closure callable) { + HibernateCallback hibernateCallback = DefaultGroovyMethods.asType(callable, HibernateCallback.class); + return execute(hibernateCallback); + } + + @Override + public T executeWithNewSession(final Closure callable) { + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + SessionHolder previousHolder = sessionHolder; + ConnectionHolder previousConnectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); + Session newSession = null; + boolean previousActiveSynchronization = TransactionSynchronizationManager.isSynchronizationActive(); + List transactionSynchronizations = previousActiveSynchronization ? TransactionSynchronizationManager.getSynchronizations() : null; + try { + // if there are any previous synchronizations active we need to clear them and restore them later (see finally block) + if (previousActiveSynchronization) { + TransactionSynchronizationManager.clearSynchronization(); + // init a new synchronization to ensure that any opened database connections are closed by the synchronization + TransactionSynchronizationManager.initSynchronization(); + } + + // if there are already bound holders, unbind them so they can be restored later + if (sessionHolder != null) { + TransactionSynchronizationManager.unbindResource(sessionFactory); + if (previousConnectionHolder != null) { + TransactionSynchronizationManager.unbindResource(dataSource); + } + } + + // create and bind a new session holder for the new session + newSession = sessionFactory.openSession(); + applyFlushMode(newSession, false); + sessionHolder = new SessionHolder(newSession); + TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); + + return execute(callable::call); + } + finally { + try { + // if an active synchronization was registered during the life time of the new session clear it + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.clearSynchronization(); + } + // If there is a synchronization active then leave it to the synchronization to close the session + if (newSession != null) { + SessionFactoryUtils.closeSession(newSession); + } + + // Clear any bound sessions and connections + TransactionSynchronizationManager.unbindResource(sessionFactory); + ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResourceIfPossible(dataSource); + // if there is a connection holder and it holds an open connection close it + try { + if (connectionHolder != null && !connectionHolder.getConnection().isClosed()) { + Connection conn = connectionHolder.getConnection(); + DataSourceUtils.releaseConnection(conn, dataSource); + } + } catch (SQLException e) { + // ignore, connection closed already? + if (LOG.isDebugEnabled()) { + LOG.debug("Could not close opened JDBC connection. Did the application close the connection manually?: " + e.getMessage()); + } + } + } + finally { + // if there were previously active synchronizations then register those again + if (previousActiveSynchronization) { + TransactionSynchronizationManager.initSynchronization(); + for (TransactionSynchronization transactionSynchronization : transactionSynchronizations) { + TransactionSynchronizationManager.registerSynchronization(transactionSynchronization); + } + } + + // now restore any previous state + if (previousHolder != null) { + TransactionSynchronizationManager.bindResource(sessionFactory, previousHolder); + if (previousConnectionHolder != null) { + TransactionSynchronizationManager.bindResource(dataSource, previousConnectionHolder); + } + } + + } + } + } + + @Override + public T1 executeWithExistingOrCreateNewSession(SessionFactory sessionFactory, Closure callable) { + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + if (sessionHolder == null) { + return executeWithNewSession(callable); + } + else { + return callable.call(sessionHolder.getSession()); + } + } + + public SessionFactory getSessionFactory() { + return sessionFactory; + } + + @Override + public void applySettings(org.hibernate.query.Query query) { + if (exposeNativeSession) { + prepareQuery(query); + } + } + + @Override + public void applySettings(Criteria criteria) { + if (exposeNativeSession) { + prepareCriteria(criteria); + } + } + + public void setCacheQueries(boolean cacheQueries) { + this.cacheQueries = cacheQueries; + } + + public boolean isCacheQueries() { + return cacheQueries; + } + + public T execute(HibernateCallback action) throws DataAccessException { + return doExecute(action, false); + } + + public List executeFind(HibernateCallback action) throws DataAccessException { + Object result = doExecute(action, false); + if (result != null && !(result instanceof List)) { + throw new InvalidDataAccessApiUsageException("Result object returned from HibernateCallback isn't a List: [" + result + "]"); + } + return (List) result; + } + + protected boolean shouldPassReadOnlyToHibernate() { + if ((passReadOnlyToHibernate || osivReadOnly) && TransactionSynchronizationManager.hasResource(getSessionFactory())) { + if (TransactionSynchronizationManager.isActualTransactionActive()) { + return passReadOnlyToHibernate && TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + } else { + return osivReadOnly; + } + } else { + return false; + } + } + + public boolean isOsivReadOnly() { + return osivReadOnly; + } + + public void setOsivReadOnly(boolean osivReadOnly) { + this.osivReadOnly = osivReadOnly; + } + + /** + * Execute the action specified by the given action object within a Session. + * + * @param action callback object that specifies the Hibernate action + * @param enforceNativeSession whether to enforce exposure of the native Hibernate Session to callback code + * @return a result object returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + */ + protected T doExecute(HibernateCallback action, boolean enforceNativeSession) throws DataAccessException { + + Assert.notNull(action, "Callback object must not be null"); + + Session session = getSession(); + boolean existingTransaction = isSessionTransactional(session); + if (existingTransaction) { + LOG.debug("Found thread-bound Session for HibernateTemplate"); + } + + FlushMode previousFlushMode = null; + try { + previousFlushMode = applyFlushMode(session, existingTransaction); + if (shouldPassReadOnlyToHibernate()) { + session.setDefaultReadOnly(true); + } + Session sessionToExpose = (enforceNativeSession || exposeNativeSession ? session : createSessionProxy(session)); + T result = action.doInHibernate(sessionToExpose); + flushIfNecessary(session, existingTransaction); + return result; + } catch (HibernateException ex) { + throw convertHibernateAccessException(ex); + } + catch (PersistenceException ex) { + if (ex.getCause() instanceof HibernateException) { + throw SessionFactoryUtils.convertHibernateAccessException((HibernateException) ex.getCause()); + } + throw ex; + } + catch (SQLException ex) { + throw jdbcExceptionTranslator.translate("Hibernate-related JDBC operation", null, ex); + } catch (RuntimeException ex) { + // Callback code threw application exception... + throw ex; + } finally { + if (existingTransaction) { + LOG.debug("Not closing pre-bound Hibernate Session after HibernateTemplate"); + if (previousFlushMode != null) { + session.setHibernateFlushMode(previousFlushMode); + } + } else { + SessionFactoryUtils.closeSession(session); + } + } + } + + protected boolean isSessionTransactional(Session session) { + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + return sessionHolder != null && sessionHolder.getSession() == session; + } + + protected Session getSession() { + try { + return sessionFactory.getCurrentSession(); + } catch (HibernateException ex) { + throw new DataAccessResourceFailureException("Could not obtain current Hibernate Session", ex); + } + } + + /** + * Create a close-suppressing proxy for the given Hibernate Session. The + * proxy also prepares returned Query and Criteria objects. + * + * @param session the Hibernate Session to create a proxy for + * @return the Session proxy + * @see org.hibernate.Session#close() + * @see #prepareQuery + * @see #prepareCriteria + */ + protected Session createSessionProxy(Session session) { + Class[] sessionIfcs; + Class mainIfc = Session.class; + if (session instanceof EventSource) { + sessionIfcs = new Class[]{mainIfc, EventSource.class}; + } else if (session instanceof SessionImplementor) { + sessionIfcs = new Class[]{mainIfc, SessionImplementor.class}; + } else { + sessionIfcs = new Class[]{mainIfc}; + } + return (Session) Proxy.newProxyInstance(session.getClass().getClassLoader(), sessionIfcs, + new CloseSuppressingInvocationHandler(session)); + } + + public T get(final Class entityClass, final Serializable id) throws DataAccessException { + return doExecute(session -> session.get(entityClass, id), true); + } + + public T get(final Class entityClass, final Serializable id, final LockMode mode) { + return lock(entityClass, id, mode); + } + + public void delete(final Object entity) throws DataAccessException { + doExecute(session -> { + session.delete(entity); + return null; + }, true); + } + + public void flush(final Object entity) throws DataAccessException { + doExecute(session -> { + session.flush(); + return null; + }, true); + } + + public T load(final Class entityClass, final Serializable id) throws DataAccessException { + return doExecute(session -> session.load(entityClass, id), true); + } + + public T lock(final Class entityClass, final Serializable id, final LockMode lockMode) throws DataAccessException { + return doExecute(session -> session.get(entityClass, id, new LockOptions(lockMode)), true); + } + + public List loadAll(final Class entityClass) throws DataAccessException { + return doExecute(session -> { + final CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + final CriteriaQuery query = criteriaBuilder.createQuery(entityClass); + final Root root = query.from(entityClass); + final Query jpaQuery = session.createQuery(query); + prepareCriteria(jpaQuery); + return jpaQuery.getResultList(); + }, true); + } + + public boolean contains(final Object entity) throws DataAccessException { + return doExecute(session -> session.contains(entity), true); + } + + public void evict(final Object entity) throws DataAccessException { + doExecute(session -> { + session.evict(entity); + return null; + }, true); + } + + public void lock(final Object entity, final LockMode lockMode) throws DataAccessException { + doExecute(session -> { + session.buildLockRequest(new LockOptions(lockMode)).lock(entity); //LockMode.PESSIMISTIC_WRITE + return null; + }, true); + } + + public void refresh(final Object entity) throws DataAccessException { + refresh(entity, null); + } + + public void refresh(final Object entity, final LockMode lockMode) throws DataAccessException { + doExecute(session -> { + if (lockMode == null) { + session.refresh(entity); + } else { + session.refresh(entity, new LockOptions(lockMode)); + } + return null; + }, true); + } + + public void setExposeNativeSession(boolean exposeNativeSession) { + this.exposeNativeSession = exposeNativeSession; + } + + public boolean isExposeNativeSession() { + return exposeNativeSession; + } + + /** + * Prepare the given Query object, applying cache settings and/or a + * transaction timeout. + * + * @param query the Query object to prepare + */ + protected void prepareQuery(org.hibernate.query.Query query) { + if (cacheQueries) { + query.setCacheable(true); + } + if (shouldPassReadOnlyToHibernate()) { + query.setReadOnly(true); + } + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + if (sessionHolder != null && sessionHolder.hasTimeout()) { + query.setTimeout(sessionHolder.getTimeToLiveInSeconds()); + } + } + + /** + * Prepare the given Criteria object, applying cache settings and/or a + * transaction timeout. + * + * @param criteria the Criteria object to prepare + * @deprecated Deprecated because Hibernate Criteria are deprecated + */ + @Deprecated + protected void prepareCriteria(Criteria criteria) { + if (cacheQueries) { + criteria.setCacheable(true); + } + if (shouldPassReadOnlyToHibernate()) { + criteria.setReadOnly(true); + } + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + if (sessionHolder != null && sessionHolder.hasTimeout()) { + criteria.setTimeout(sessionHolder.getTimeToLiveInSeconds()); + } + } + + /** + * Prepare the given Query object, applying cache settings and/or a + * transaction timeout. + * + * @param jpaQuery the Query object to prepare + */ + protected void prepareCriteria(Query jpaQuery) { + if (cacheQueries) { + jpaQuery.setCacheable(true); + } + if (shouldPassReadOnlyToHibernate()) { + jpaQuery.setReadOnly(true); + } + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + if (sessionHolder != null && sessionHolder.hasTimeout()) { + jpaQuery.setTimeout(sessionHolder.getTimeToLiveInSeconds()); + } + } + + /** + * Invocation handler that suppresses close calls on Hibernate Sessions. + * Also prepares returned Query and Criteria objects. + * + * @see org.hibernate.Session#close + */ + protected class CloseSuppressingInvocationHandler implements InvocationHandler { + + protected final Session target; + + protected CloseSuppressingInvocationHandler(Session target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on Session interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } + if (method.getName().equals("hashCode")) { + // Use hashCode of Session proxy. + return System.identityHashCode(proxy); + } + if (method.getName().equals("close")) { + // Handle close method: suppress, not valid. + return null; + } + + // Invoke method on target Session. + try { + Object retVal = method.invoke(target, args); + + // If return value is a Query or Criteria, apply transaction timeout. + // Applies to createQuery, getNamedQuery, createCriteria. + if (retVal instanceof org.hibernate.query.Query) { + prepareQuery(((org.hibernate.query.Query) retVal)); + } + if (retVal instanceof Criteria) { + prepareCriteria(((Criteria) retVal)); + } else if (retVal instanceof Query) { + prepareCriteria(((Query) retVal)); + } + + return retVal; + } catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + /** + * Never flush is a good strategy for read-only units of work. + * Hibernate will not track and look for changes in this case, + * avoiding any overhead of modification detection. + *

In case of an existing Session, FLUSH_NEVER will turn the flush mode + * to NEVER for the scope of the current operation, resetting the previous + * flush mode afterwards. + * + * @see #setFlushMode + */ + public static final int FLUSH_NEVER = 0; + + /** + * Automatic flushing is the default mode for a Hibernate Session. + * A session will get flushed on transaction commit, and on certain find + * operations that might involve already modified instances, but not + * after each unit of work like with eager flushing. + *

In case of an existing Session, FLUSH_AUTO will participate in the + * existing flush mode, not modifying it for the current operation. + * This in particular means that this setting will not modify an existing + * flush mode NEVER, in contrast to FLUSH_EAGER. + * + * @see #setFlushMode + */ + public static final int FLUSH_AUTO = 1; + + /** + * Eager flushing leads to immediate synchronization with the database, + * even if in a transaction. This causes inconsistencies to show up and throw + * a respective exception immediately, and JDBC access code that participates + * in the same transaction will see the changes as the database is already + * aware of them then. But the drawbacks are: + *

    + *
  • additional communication roundtrips with the database, instead of a + * single batch at transaction commit; + *
  • the fact that an actual database rollback is needed if the Hibernate + * transaction rolls back (due to already submitted SQL statements). + *
+ *

In case of an existing Session, FLUSH_EAGER will turn the flush mode + * to AUTO for the scope of the current operation and issue a flush at the + * end, resetting the previous flush mode afterwards. + * + * @see #setFlushMode + */ + public static final int FLUSH_EAGER = 2; + + /** + * Flushing at commit only is intended for units of work where no + * intermediate flushing is desired, not even for find operations + * that might involve already modified instances. + *

In case of an existing Session, FLUSH_COMMIT will turn the flush mode + * to COMMIT for the scope of the current operation, resetting the previous + * flush mode afterwards. The only exception is an existing flush mode + * NEVER, which will not be modified through this setting. + * + * @see #setFlushMode + */ + public static final int FLUSH_COMMIT = 3; + + /** + * Flushing before every query statement is rarely necessary. + * It is only available for special needs. + *

In case of an existing Session, FLUSH_ALWAYS will turn the flush mode + * to ALWAYS for the scope of the current operation, resetting the previous + * flush mode afterwards. + * + * @see #setFlushMode + */ + public static final int FLUSH_ALWAYS = 4; + + /** + * Set the flush behavior to one of the constants in this class. Default is + * FLUSH_AUTO. + * + * @see #FLUSH_AUTO + */ + public void setFlushMode(int flushMode) { + this.flushMode = flushMode; + } + + /** + * Return if a flush should be forced after executing the callback code. + */ + public int getFlushMode() { + return flushMode; + } + + /** + * Apply the flush mode that's been specified for this accessor to the given Session. + * + * @param session the current Hibernate Session + * @param existingTransaction if executing within an existing transaction + * @return the previous flush mode to restore after the operation, or null if none + * @see #setFlushMode + * @see org.hibernate.Session#setFlushMode + */ + protected FlushMode applyFlushMode(Session session, boolean existingTransaction) { + if (isApplyFlushModeOnlyToNonExistingTransactions() && existingTransaction) { + return null; + } + + if (getFlushMode() == FLUSH_NEVER) { + if (existingTransaction) { + FlushMode previousFlushMode = session.getHibernateFlushMode(); + if (!previousFlushMode.lessThan(FlushMode.COMMIT)) { + session.setHibernateFlushMode(FlushMode.MANUAL); + return previousFlushMode; + } + } else { + session.setHibernateFlushMode(FlushMode.MANUAL); + } + } else if (getFlushMode() == FLUSH_EAGER) { + if (existingTransaction) { + FlushMode previousFlushMode = session.getHibernateFlushMode(); + if (!previousFlushMode.equals(FlushMode.AUTO)) { + session.setHibernateFlushMode(FlushMode.AUTO); + return previousFlushMode; + } + } else { + // rely on default FlushMode.AUTO + } + } else if (getFlushMode() == FLUSH_COMMIT) { + if (existingTransaction) { + FlushMode previousFlushMode = session.getHibernateFlushMode(); + if (previousFlushMode.equals(FlushMode.AUTO) || previousFlushMode.equals(FlushMode.ALWAYS)) { + session.setHibernateFlushMode(FlushMode.COMMIT); + return previousFlushMode; + } + } else { + session.setHibernateFlushMode(FlushMode.COMMIT); + } + } else if (getFlushMode() == FLUSH_ALWAYS) { + if (existingTransaction) { + FlushMode previousFlushMode = session.getHibernateFlushMode(); + if (!previousFlushMode.equals(FlushMode.ALWAYS)) { + session.setHibernateFlushMode(FlushMode.ALWAYS); + return previousFlushMode; + } + } else { + session.setHibernateFlushMode(FlushMode.ALWAYS); + } + } + return null; + } + + protected void flushIfNecessary(Session session, boolean existingTransaction) throws HibernateException { + if (getFlushMode() == FLUSH_EAGER || (!existingTransaction && getFlushMode() != FLUSH_NEVER)) { + LOG.debug("Eagerly flushing Hibernate session"); + session.flush(); + } + } + + @SuppressWarnings("ConstantConditions") + protected DataAccessException convertHibernateAccessException(HibernateException ex) { + if (ex instanceof JDBCException) { + return convertJdbcAccessException((JDBCException) ex, jdbcExceptionTranslator); + } + if (GenericJDBCException.class.equals(ex.getClass())) { + return convertJdbcAccessException((GenericJDBCException) ex, jdbcExceptionTranslator); + } + return SessionFactoryUtils.convertHibernateAccessException(ex); + } + + @SuppressWarnings("SqlDialectInspection") + protected DataAccessException convertJdbcAccessException(JDBCException ex, SQLExceptionTranslator translator) { + String msg = ex.getMessage(); + String sql = ex.getSQL(); + SQLException sqlException = ex.getSQLException(); + return translator.translate("Hibernate operation: " + msg, sql, sqlException); + } + + public Serializable save(Object o) { + return sessionFactory.getCurrentSession().save(o); + } + + public void flush() { + sessionFactory.getCurrentSession().flush(); + } + + public void clear() { + sessionFactory.getCurrentSession().clear(); + } + + public void deleteAll(final Collection objects) { + execute((HibernateCallback) session -> { + for (Object entity : getIterableAsCollection(objects)) { + session.delete(entity); + } + return null; + }); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected Collection getIterableAsCollection(Iterable objects) { + Collection list; + if (objects instanceof Collection) { + list = (Collection) objects; + } else { + list = new ArrayList(); + for (Object object : objects) { + list.add(object); + } + } + return list; + } + + public boolean isApplyFlushModeOnlyToNonExistingTransactions() { + return applyFlushModeOnlyToNonExistingTransactions; + } + + public void setApplyFlushModeOnlyToNonExistingTransactions(boolean applyFlushModeOnlyToNonExistingTransactions) { + this.applyFlushModeOnlyToNonExistingTransactions = applyFlushModeOnlyToNonExistingTransactions; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTransactionManager.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTransactionManager.groovy new file mode 100644 index 00000000000..00fd6838459 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTransactionManager.groovy @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import javax.sql.DataSource + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +import org.hibernate.FlushMode +import org.hibernate.Session +import org.hibernate.SessionFactory +import org.hibernate.engine.jdbc.spi.JdbcCoordinator +import org.hibernate.engine.spi.SessionImplementor + +import org.grails.orm.hibernate.support.hibernate5.HibernateTransactionManager +import org.grails.orm.hibernate.support.hibernate5.SessionHolder +import org.springframework.transaction.TransactionDefinition +import org.springframework.transaction.support.DefaultTransactionStatus +import org.springframework.transaction.support.TransactionSynchronizationManager +import org.springframework.util.Assert + +/** + * Extends the standard class to always set the flush mode to manual when in a read-only transaction. + * + * @author Burt Beckwith + */ +@CompileStatic +@Slf4j +class GrailsHibernateTransactionManager extends HibernateTransactionManager { + + final FlushMode defaultFlushMode + boolean isJdbcBatchVersionedData + + GrailsHibernateTransactionManager(FlushMode defaultFlushMode = FlushMode.AUTO) { + this.defaultFlushMode = defaultFlushMode + } + + GrailsHibernateTransactionManager(SessionFactory sessionFactory, FlushMode defaultFlushMode = FlushMode.AUTO) { + super(sessionFactory) + this.defaultFlushMode = defaultFlushMode + this.isJdbcBatchVersionedData = sessionFactory.getSessionFactoryOptions().isJdbcBatchVersionedData() + } + + GrailsHibernateTransactionManager(SessionFactory sessionFactory, DataSource dataSource, FlushMode defaultFlushMode = FlushMode.AUTO) { + super(sessionFactory) + setDataSource(dataSource) + this.defaultFlushMode = defaultFlushMode + this.isJdbcBatchVersionedData = sessionFactory.getSessionFactoryOptions().isJdbcBatchVersionedData() + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + super.doBegin(transaction, definition) + + if (definition.isReadOnly()) { + // transaction is HibernateTransactionManager.HibernateTransactionObject private class instance + // always set to manual; the base class doesn't because the OSIV has already registered a session + + SessionHolder holder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory) + holder.session.setHibernateFlushMode(FlushMode.MANUAL) + } + else if (defaultFlushMode != FlushMode.AUTO) { + SessionHolder holder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory) + holder.session.setHibernateFlushMode(defaultFlushMode) + } + } + + @Override + protected void doRollback(DefaultTransactionStatus status) { + super.doRollback(status) + if (isJdbcBatchVersionedData) { + try { + SessionHolder holder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory) + if (holder != null) { + Session session = holder.getSession() + JdbcCoordinator jdbcCoordinator = ((SessionImplementor) session).getJdbcCoordinator() + jdbcCoordinator.abortBatch() + } + } catch (Throwable e) { + log.warn("Error aborting batch during Transaction rollback: ${e.message}", e) + } + } + } + + @Override + void setSessionFactory(SessionFactory sessionFactory) { + Assert.notNull(sessionFactory, 'SessionFactory cannot be null') + super.setSessionFactory(sessionFactory) + this.isJdbcBatchVersionedData = sessionFactory.getSessionFactoryOptions().isJdbcBatchVersionedData() + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsSessionContext.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsSessionContext.java new file mode 100644 index 00000000000..7f4943667c1 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsSessionContext.java @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import jakarta.transaction.Status; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionManager; + +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.context.spi.CurrentSessionContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; +import org.hibernate.service.spi.ServiceBinding; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.transaction.jta.SpringJtaSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import org.grails.orm.hibernate.support.hibernate5.SessionHolder; +import org.grails.orm.hibernate.support.hibernate5.SpringFlushSynchronization; +import org.grails.orm.hibernate.support.hibernate5.SpringJtaSessionContext; +import org.grails.orm.hibernate.support.hibernate5.SpringSessionSynchronization; + +/** + * Based on org.springframework.orm.hibernate4.SpringSessionContext. + * + * @author Juergen Hoeller + * @author Burt Beckwith + */ +public class GrailsSessionContext implements CurrentSessionContext { + + private static final long serialVersionUID = 1; + + private static final Logger LOG = LoggerFactory.getLogger(GrailsSessionContext.class); + + protected final SessionFactoryImplementor sessionFactory; + protected CurrentSessionContext jtaSessionContext; + + // TODO make configurable? + protected boolean allowCreate = false; + + /** + * Constructor. + * @param sessionFactory the SessionFactory to provide current Sessions for + */ + public GrailsSessionContext(SessionFactoryImplementor sessionFactory) { + this.sessionFactory = sessionFactory; + } + + public void initJta() { + JtaPlatform jtaPlatform = sessionFactory.getServiceRegistry().getService(JtaPlatform.class); + TransactionManager transactionManager = jtaPlatform.retrieveTransactionManager(); + jtaSessionContext = transactionManager == null ? null : new SpringJtaSessionContext(sessionFactory); + } + + /** + * Retrieve the Spring-managed Session for the current thread, if any. + */ + public Session currentSession() throws HibernateException { + Object value = TransactionSynchronizationManager.getResource(sessionFactory); + if (value instanceof Session) { + return (Session) value; + } + + if (value instanceof SessionHolder) { + SessionHolder sessionHolder = (SessionHolder) value; + Session session = sessionHolder.getSession(); + if (TransactionSynchronizationManager.isSynchronizationActive() && !sessionHolder.isSynchronizedWithTransaction()) { + TransactionSynchronizationManager.registerSynchronization(createSpringSessionSynchronization(sessionHolder)); + sessionHolder.setSynchronizedWithTransaction(true); + // Switch to FlushMode.AUTO, as we have to assume a thread-bound Session + // with FlushMode.MANUAL, which needs to allow flushing within the transaction. + FlushMode flushMode = session.getHibernateFlushMode(); + if (flushMode.equals(FlushMode.MANUAL) && !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + session.setHibernateFlushMode(FlushMode.AUTO); + sessionHolder.setPreviousFlushMode(flushMode); + } + } + return session; + } + + if (jtaSessionContext != null) { + Session session = jtaSessionContext.currentSession(); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(createSpringFlushSynchronization(session)); + } + return session; + } + + if (allowCreate) { + // be consistent with older HibernateTemplate behavior + return createSession(value); + } + + throw new HibernateException("No Session found for current thread"); + } + + private Session createSession(Object resource) { + LOG.debug("Opening Hibernate Session"); + + SessionHolder sessionHolder = (SessionHolder) resource; + + Session session = sessionFactory.openSession(); + + // Use same Session for further Hibernate actions within the transaction. + // Thread object will get removed by synchronization at transaction completion. + if (TransactionSynchronizationManager.isSynchronizationActive()) { + // We're within a Spring-managed transaction, possibly from JtaTransactionManager. + LOG.debug("Registering Spring transaction synchronization for new Hibernate Session"); + SessionHolder holderToUse = sessionHolder; + if (holderToUse == null) { + holderToUse = new SessionHolder(session); + } + else { + // it's up to the caller to manage concurrent sessions + // holderToUse.addSession(session); + } + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + session.setHibernateFlushMode(FlushMode.MANUAL); + } + TransactionSynchronizationManager.registerSynchronization(createSpringSessionSynchronization(holderToUse)); + holderToUse.setSynchronizedWithTransaction(true); + if (holderToUse != sessionHolder) { + TransactionSynchronizationManager.bindResource(sessionFactory, holderToUse); + } + } + else { + // No Spring transaction management active -> try JTA transaction synchronization. + registerJtaSynchronization(session, sessionHolder); + } + + /* + // Check whether we are allowed to return the Session. + if (!allowCreate && !isSessionTransactional(session, sessionFactory)) { + closeSession(session); + throw new IllegalStateException("No Hibernate Session bound to thread, " + + "and configuration does not allow creation of non-transactional one here"); + } + */ + return session; + } + + protected void registerJtaSynchronization(Session session, SessionHolder sessionHolder) { + + // JTA synchronization is only possible with a jakarta.transaction.TransactionManager. + // We'll check the Hibernate SessionFactory: If a TransactionManagerLookup is specified + // in Hibernate configuration, it will contain a TransactionManager reference. + TransactionManager jtaTm = getJtaTransactionManager(session); + if (jtaTm == null) { + return; + } + + try { + Transaction jtaTx = jtaTm.getTransaction(); + if (jtaTx == null) { + return; + } + + int jtaStatus = jtaTx.getStatus(); + if (jtaStatus != Status.STATUS_ACTIVE && jtaStatus != Status.STATUS_MARKED_ROLLBACK) { + return; + } + + LOG.debug("Registering JTA transaction synchronization for new Hibernate Session"); + SessionHolder holderToUse = sessionHolder; + // Register JTA Transaction with existing SessionHolder. + // Create a new SessionHolder if none existed before. + if (holderToUse == null) { + holderToUse = new SessionHolder(session); + } + else { + // it's up to the caller to manage concurrent sessions + // holderToUse.addSession(session); + } + jtaTx.registerSynchronization(new SpringJtaSynchronizationAdapter(createSpringSessionSynchronization(holderToUse), jtaTm)); + holderToUse.setSynchronizedWithTransaction(true); + if (holderToUse != sessionHolder) { + TransactionSynchronizationManager.bindResource(sessionFactory, holderToUse); + } + } + catch (Throwable ex) { + throw new DataAccessResourceFailureException("Could not register synchronization with JTA TransactionManager", ex); + } + } + + protected TransactionManager getJtaTransactionManager(Session session) { + SessionFactoryImplementor sessionFactoryImpl = null; + if (sessionFactory instanceof SessionFactoryImplementor) { + sessionFactoryImpl = ((SessionFactoryImplementor) sessionFactory); + } + else if (session != null) { + SessionFactory internalFactory = session.getSessionFactory(); + if (internalFactory instanceof SessionFactoryImplementor) { + sessionFactoryImpl = (SessionFactoryImplementor) internalFactory; + } + } + + if (sessionFactoryImpl == null) { + return null; + } + + ServiceBinding sb = sessionFactory.getServiceRegistry().locateServiceBinding(JtaPlatform.class); + if (sb == null) { + return null; + } + + return sb.getService().retrieveTransactionManager(); + } + + protected TransactionSynchronization createSpringFlushSynchronization(Session session) { + return new SpringFlushSynchronization(session); + } + + protected TransactionSynchronization createSpringSessionSynchronization(SessionHolder sessionHolder) { + return new SpringSessionSynchronization(sessionHolder, sessionFactory); + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java new file mode 100644 index 00000000000..7eb3e337a08 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java @@ -0,0 +1,690 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import java.io.IOException; +import java.io.Serializable; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +import javax.sql.DataSource; + +import org.hibernate.SessionFactory; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.SchemaAutoTooling; +import org.hibernate.cfg.Environment; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.integrator.spi.IntegratorService; +import org.hibernate.service.ServiceRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.core.env.PropertyResolver; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import grails.gorm.MultiTenant; +import org.grails.datastore.gorm.events.AutoTimestampEventListener; +import org.grails.datastore.gorm.events.ConfigurableApplicationContextEventPublisher; +import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher; +import org.grails.datastore.gorm.events.DefaultApplicationEventPublisher; +import org.grails.datastore.gorm.jdbc.MultiTenantConnection; +import org.grails.datastore.gorm.jdbc.MultiTenantDataSource; +import org.grails.datastore.gorm.jdbc.connections.DataSourceConnectionSource; +import org.grails.datastore.gorm.jdbc.connections.DataSourceConnectionSourceFactory; +import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings; +import org.grails.datastore.gorm.utils.ClasspathEntityScanner; +import org.grails.datastore.gorm.validation.constraints.MappingContextAwareConstraintFactory; +import org.grails.datastore.gorm.validation.constraints.builtin.UniqueConstraint; +import org.grails.datastore.gorm.validation.constraints.registry.ConstraintRegistry; +import org.grails.datastore.mapping.core.ConnectionNotFoundException; +import org.grails.datastore.mapping.core.Datastore; +import org.grails.datastore.mapping.core.DatastoreUtils; +import org.grails.datastore.mapping.core.Session; +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.datastore.mapping.core.connections.ConnectionSources; +import org.grails.datastore.mapping.core.connections.ConnectionSourcesInitializer; +import org.grails.datastore.mapping.core.connections.DefaultConnectionSource; +import org.grails.datastore.mapping.core.connections.SingletonConnectionSources; +import org.grails.datastore.mapping.core.exceptions.ConfigurationException; +import org.grails.datastore.mapping.engine.event.DatastoreInitializedEvent; +import org.grails.datastore.mapping.model.DatastoreConfigurationException; +import org.grails.datastore.mapping.model.MappingContext; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.multitenancy.AllTenantsResolver; +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings; +import org.grails.datastore.mapping.validation.ValidatorRegistry; +import org.grails.orm.hibernate.cfg.GrailsDomainBinder; +import org.grails.orm.hibernate.cfg.HibernateMappingContext; +import org.grails.orm.hibernate.cfg.Settings; +import org.grails.orm.hibernate.connections.HibernateConnectionSource; +import org.grails.orm.hibernate.connections.HibernateConnectionSourceFactory; +import org.grails.orm.hibernate.connections.HibernateConnectionSourceSettings; +import org.grails.orm.hibernate.event.listener.HibernateEventListener; +import org.grails.orm.hibernate.multitenancy.MultiTenantEventListener; +import org.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor; + +/** + * Datastore implementation that uses a Hibernate SessionFactory underneath. + * + * @author Graeme Rocher + * @since 2.0 + */ +public class HibernateDatastore extends AbstractHibernateDatastore implements MessageSourceAware { + private static final Logger LOG = LoggerFactory.getLogger(HibernateDatastore.class); + + protected final GrailsHibernateTransactionManager transactionManager; + protected ConfigurableApplicationEventPublisher eventPublisher; + protected final HibernateGormEnhancer gormEnhancer; + protected final Map datastoresByConnectionSource = new LinkedHashMap<>(); + protected final Metadata metadata; + + /** + * Create a new HibernateDatastore for the given connection sources and mapping context + * + * @param connectionSources The {@link ConnectionSources} instance + * @param mappingContext The {@link MappingContext} instance + * @param eventPublisher The {@link ConfigurableApplicationEventPublisher} instance + */ + public HibernateDatastore(final ConnectionSources connectionSources, final HibernateMappingContext mappingContext, final ConfigurableApplicationEventPublisher eventPublisher) { + super(connectionSources, mappingContext); + + this.metadata = getMetadataInternal(); + + HibernateConnectionSource defaultConnectionSource = (HibernateConnectionSource) connectionSources.getDefaultConnectionSource(); + this.transactionManager = new GrailsHibernateTransactionManager( + defaultConnectionSource.getSource(), + defaultConnectionSource.getDataSource(), + org.hibernate.FlushMode.valueOf(defaultFlushModeName)); + this.eventPublisher = eventPublisher; + this.eventTriggeringInterceptor = new HibernateEventListener(this); + this.autoTimestampEventListener = new AutoTimestampEventListener(this); + + HibernateConnectionSourceSettings settings = defaultConnectionSource.getSettings(); + HibernateConnectionSourceSettings.HibernateSettings hibernateSettings = settings.getHibernate(); + + ClosureEventTriggeringInterceptor interceptor = (ClosureEventTriggeringInterceptor) hibernateSettings.getEventTriggeringInterceptor(); + interceptor.setDatastore(this); + interceptor.setEventPublisher(eventPublisher); + registerEventListeners(this.eventPublisher); + configureValidatorRegistry(settings, mappingContext); + this.mappingContext.addMappingContextListener(new MappingContext.Listener() { + @Override + public void persistentEntityAdded(PersistentEntity entity) { + gormEnhancer.registerEntity(entity); + } + }); + initializeConverters(this.mappingContext); + + if (!(connectionSources instanceof SingletonConnectionSources)) { + + final HibernateDatastore parent = this; + Iterable> allConnectionSources = connectionSources.getAllConnectionSources(); + for (ConnectionSource connectionSource : allConnectionSources) { + SingletonConnectionSources singletonConnectionSources = new SingletonConnectionSources<>(connectionSource, connectionSources.getBaseConfiguration()); + HibernateDatastore childDatastore; + + if (ConnectionSource.DEFAULT.equals(connectionSource.getName())) { + childDatastore = this; + } else { + childDatastore = createChildDatastore(mappingContext, eventPublisher, parent, singletonConnectionSources); + } + datastoresByConnectionSource.put(connectionSource.getName(), childDatastore); + } + + // register a listener to update the datastore each time a connection source is added at runtime + connectionSources.addListener(connectionSource -> { + SingletonConnectionSources singletonConnectionSources = new SingletonConnectionSources<>(connectionSource, connectionSources.getBaseConfiguration()); + HibernateDatastore childDatastore = createChildDatastore(mappingContext, eventPublisher, parent, singletonConnectionSources); + datastoresByConnectionSource.put(connectionSource.getName(), childDatastore); + registerAllEntitiesWithEnhancer(); + }); + + if (multiTenantMode == MultiTenancySettings.MultiTenancyMode.SCHEMA) { + if (this.tenantResolver instanceof AllTenantsResolver) { + AllTenantsResolver allTenantsResolver = (AllTenantsResolver) tenantResolver; + Iterable tenantIds = allTenantsResolver.resolveTenantIds(); + + for (Serializable tenantId : tenantIds) { + addTenantForSchemaInternal(tenantId.toString()); + } + } + else { + Collection allSchemas = schemaHandler.resolveSchemaNames(defaultConnectionSource.getDataSource()); + for (String schema : allSchemas) { + addTenantForSchemaInternal(schema); + } + } + } + } + + this.gormEnhancer = initialize(); + } + + private HibernateDatastore createChildDatastore(HibernateMappingContext mappingContext, + ConfigurableApplicationEventPublisher eventPublisher, + HibernateDatastore parent, + SingletonConnectionSources singletonConnectionSources) { + return new HibernateDatastore(singletonConnectionSources, mappingContext, eventPublisher) { + @Override + protected HibernateGormEnhancer initialize() { + return null; + } + + @Override + public HibernateDatastore getDatastoreForConnection(String connectionName) { + if (connectionName.equals(Settings.SETTING_DATASOURCE) || connectionName.equals(ConnectionSource.DEFAULT)) { + return parent; + } else { + HibernateDatastore hibernateDatastore = parent.datastoresByConnectionSource.get(connectionName); + if (hibernateDatastore == null) { + throw new ConfigurationException("DataSource not found for name [" + connectionName + "] in configuration. Please check your multiple data sources configuration and try again."); + } + return hibernateDatastore; + } + } + }; + } + + /** + * Create a new HibernateDatastore for the given connection sources and mapping context + * + * @param configuration The configuration + * @param connectionSourceFactory The {@link HibernateConnectionSourceFactory} instance + * @param eventPublisher The {@link ConfigurableApplicationEventPublisher} instance + */ + public HibernateDatastore(PropertyResolver configuration, HibernateConnectionSourceFactory connectionSourceFactory, ConfigurableApplicationEventPublisher eventPublisher) { + this(ConnectionSourcesInitializer.create(connectionSourceFactory, DatastoreUtils.preparePropertyResolver(configuration, "dataSource", "hibernate", "grails")), connectionSourceFactory.getMappingContext(), eventPublisher); + } + + /** + * Create a new HibernateDatastore for the given connection sources and mapping context + * + * @param configuration The configuration + * @param connectionSourceFactory The {@link HibernateConnectionSourceFactory} instance + */ + public HibernateDatastore(PropertyResolver configuration, HibernateConnectionSourceFactory connectionSourceFactory) { + this(ConnectionSourcesInitializer.create(connectionSourceFactory, DatastoreUtils.preparePropertyResolver(configuration, "dataSource", "hibernate", "grails")), connectionSourceFactory.getMappingContext(), new DefaultApplicationEventPublisher()); + } + + /** + * Create a new HibernateDatastore for the given connection sources and mapping context + * + * @param configuration The configuration + * @param eventPublisher The {@link ConfigurableApplicationEventPublisher} instance + * @param classes The persistent classes + */ + public HibernateDatastore(PropertyResolver configuration, ConfigurableApplicationEventPublisher eventPublisher, Class... classes) { + this(configuration, new HibernateConnectionSourceFactory(classes), eventPublisher); + } + + /** + * Create a new HibernateDatastore for the given connection sources and mapping context + * + * @param configuration The configuration + * @param eventPublisher The {@link ConfigurableApplicationEventPublisher} instance + * @param classes The persistent classes + */ + public HibernateDatastore(DataSource dataSource, PropertyResolver configuration, ConfigurableApplicationEventPublisher eventPublisher, Class... classes) { + this(configuration, createConnectionFactoryForDataSource(dataSource, classes), eventPublisher); + } + + /** + * Construct a Hibernate datastore scanning the given packages + * + * @param configuration The configuration + * @param eventPublisher The event publisher + * @param packagesToScan The packages to scan + */ + public HibernateDatastore(PropertyResolver configuration, ConfigurableApplicationEventPublisher eventPublisher, Package... packagesToScan) { + this(configuration, eventPublisher, new ClasspathEntityScanner().scan(packagesToScan)); + } + + /** + * Construct a Hibernate datastore scanning the given packages for the given datasource + * + * @param configuration The configuration + * @param eventPublisher The event publisher + * @param packagesToScan The packages to scan + */ + public HibernateDatastore(DataSource dataSource, PropertyResolver configuration, ConfigurableApplicationEventPublisher eventPublisher, Package... packagesToScan) { + this(dataSource, configuration, eventPublisher, new ClasspathEntityScanner().scan(packagesToScan)); + } + + /** + * Create a new HibernateDatastore for the given connection sources and mapping context + * + * @param configuration The configuration + * @param classes The persistent classes + */ + public HibernateDatastore(PropertyResolver configuration, Class... classes) { + this(configuration, new HibernateConnectionSourceFactory(classes)); + } + + /** + * Construct a Hibernate datastore scanning the given packages + * + * @param configuration The configuration + * @param packagesToScan The packages to scan + */ + public HibernateDatastore(PropertyResolver configuration, Package... packagesToScan) { + this(configuration, new ClasspathEntityScanner().scan(packagesToScan)); + } + + /** + * Constructor used purely for testing purposes. Creates a datastore with an in-memory database and dbCreate set to 'create-drop' + * + * @param classes The classes + */ + public HibernateDatastore(Map configuration, Class... classes) { + this(DatastoreUtils.createPropertyResolver(configuration), new HibernateConnectionSourceFactory(classes)); + } + + /** + * Construct a Hibernate datastore scanning the given packages + * + * @param configuration The configuration + * @param packagesToScan The packages to scan + */ + public HibernateDatastore(Map configuration, Package... packagesToScan) { + this(DatastoreUtils.createPropertyResolver(configuration), packagesToScan); + } + + /** + * Constructor used purely for testing purposes. Creates a datastore with an in-memory database and dbCreate set to 'create-drop' + * + * @param classes The classes + */ + public HibernateDatastore(Class... classes) { + this(DatastoreUtils.createPropertyResolver(Collections.singletonMap(Settings.SETTING_DB_CREATE, "create-drop")), new HibernateConnectionSourceFactory(classes)); + } + + /** + * Construct a Hibernate datastore scanning the given packages + * + * @param packagesToScan The packages to scan + */ + public HibernateDatastore(Package... packagesToScan) { + this(new ClasspathEntityScanner().scan(packagesToScan)); + } + + /** + * Construct a Hibernate datastore scanning the given packages + * + * @param packageToScan The package to scan + */ + public HibernateDatastore(Package packageToScan) { + this(new ClasspathEntityScanner().scan(packageToScan)); + } + + @Override + public ApplicationEventPublisher getApplicationEventPublisher() { + return this.eventPublisher; + } + + /** + * @return The {@link org.springframework.transaction.PlatformTransactionManager} instance + */ + public GrailsHibernateTransactionManager getTransactionManager() { + return transactionManager; + } + + /** + * Obtain a child {@link HibernateDatastore} by connection name + * + * @param connectionName The connection name + * + * @return The {@link HibernateDatastore} + */ + public HibernateDatastore getDatastoreForConnection(String connectionName) { + if (connectionName.equals(Settings.SETTING_DATASOURCE) || connectionName.equals(ConnectionSource.DEFAULT)) { + return this; + } else { + HibernateDatastore hibernateDatastore = this.datastoresByConnectionSource.get(connectionName); + if (hibernateDatastore == null) { + throw new ConfigurationException("DataSource not found for name [" + connectionName + "] in configuration. Please check your multiple data sources configuration and try again."); + } + return hibernateDatastore; + } + } + + @Override + public String toString() { + return "HibernateDatastore: " + getDataSourceName(); + } + + @Override + public HibernateMappingContext getMappingContext() { + return (HibernateMappingContext) super.getMappingContext(); + } + + @Override + public void setMessageSource(MessageSource messageSource) { + HibernateMappingContext mappingContext = getMappingContext(); + ValidatorRegistry validatorRegistry = createValidatorRegistry(messageSource); + HibernateConnectionSourceSettings settings = getConnectionSources().getDefaultConnectionSource().getSettings(); + configureValidatorRegistry(settings, mappingContext, validatorRegistry, messageSource); + } + + protected void registerEventListeners(ConfigurableApplicationEventPublisher eventPublisher) { + eventPublisher.addApplicationListener(autoTimestampEventListener); + if (multiTenantMode == MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR) { + eventPublisher.addApplicationListener(new MultiTenantEventListener()); + } + eventPublisher.addApplicationListener(eventTriggeringInterceptor); + } + + protected void configureValidatorRegistry(HibernateConnectionSourceSettings settings, HibernateMappingContext mappingContext) { + StaticMessageSource messageSource = new StaticMessageSource(); + ValidatorRegistry defaultValidatorRegistry = createValidatorRegistry(messageSource); + configureValidatorRegistry(settings, mappingContext, defaultValidatorRegistry, messageSource); + } + + protected void configureValidatorRegistry(HibernateConnectionSourceSettings settings, HibernateMappingContext mappingContext, ValidatorRegistry validatorRegistry, MessageSource messageSource) { + if (validatorRegistry instanceof ConstraintRegistry) { + ((ConstraintRegistry) validatorRegistry).addConstraintFactory( + new MappingContextAwareConstraintFactory(UniqueConstraint.class, messageSource, mappingContext) + ); + } + mappingContext.setValidatorRegistry( + validatorRegistry + ); + } + + protected HibernateGormEnhancer initialize() { + final HibernateConnectionSource defaultConnectionSource = (HibernateConnectionSource) getConnectionSources().getDefaultConnectionSource(); + if (multiTenantMode == MultiTenancySettings.MultiTenancyMode.SCHEMA) { + return new HibernateGormEnhancer(this, transactionManager, defaultConnectionSource.getSettings()) { + @Override + public List allQualifiers(Datastore datastore, PersistentEntity entity) { + List allQualifiers = super.allQualifiers(datastore, entity); + if (MultiTenant.class.isAssignableFrom(entity.getJavaClass())) { + if (tenantResolver instanceof AllTenantsResolver) { + Iterable tenantIds = ((AllTenantsResolver) tenantResolver).resolveTenantIds(); + for (Serializable id : tenantIds) { + allQualifiers.add(id.toString()); + } + } + else { + Collection schemaNames = schemaHandler.resolveSchemaNames(defaultConnectionSource.getDataSource()); + for (String schemaName : schemaNames) { + // skip common internal schemas + if (schemaName.equals("INFORMATION_SCHEMA") || schemaName.equals("PUBLIC")) continue; + for (String connectionName : datastoresByConnectionSource.keySet()) { + if (schemaName.equalsIgnoreCase(connectionName)) { + allQualifiers.add(connectionName); + } + } + } + } + } + + return allQualifiers; + } + }; + } + else { + return new HibernateGormEnhancer(this, transactionManager, defaultConnectionSource.getSettings()); + } + } + + @Override + public boolean hasCurrentSession() { + return TransactionSynchronizationManager.getResource(sessionFactory) != null; + } + + @Override + protected Session createSession(PropertyResolver connectionDetails) { + return new HibernateSession(this, sessionFactory); + } + + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (applicationContext instanceof ConfigurableApplicationContext) { + super.setApplicationContext(applicationContext); + + for (HibernateDatastore hibernateDatastore : datastoresByConnectionSource.values()) { + if (hibernateDatastore != this) { + hibernateDatastore.setApplicationContext(applicationContext); + } + } + this.eventPublisher = new ConfigurableApplicationContextEventPublisher((ConfigurableApplicationContext) applicationContext); + HibernateConnectionSourceSettings settings = getConnectionSources().getDefaultConnectionSource().getSettings(); + HibernateConnectionSourceSettings.HibernateSettings hibernateSettings = settings.getHibernate(); + ClosureEventTriggeringInterceptor interceptor = (ClosureEventTriggeringInterceptor) hibernateSettings.getEventTriggeringInterceptor(); + interceptor.setDatastore(this); + interceptor.setEventPublisher(eventPublisher); + MappingContext mappingContext = getMappingContext(); + // make messages from the application context available to validation + ValidatorRegistry validatorRegistry = createValidatorRegistry(applicationContext); + configureValidatorRegistry(settings, (HibernateMappingContext) mappingContext, validatorRegistry, applicationContext); + mappingContext.setValidatorRegistry( + validatorRegistry + ); + + registerEventListeners(eventPublisher); + this.eventPublisher.publishEvent(new DatastoreInitializedEvent(this)); + } + } + + @Override + public IHibernateTemplate getHibernateTemplate(int flushMode) { + return new GrailsHibernateTemplate(getSessionFactory(), this, flushMode); + } + + @Override + public void withFlushMode(FlushMode flushMode, Callable callable) { + final org.hibernate.Session session = sessionFactory.getCurrentSession(); + org.hibernate.FlushMode previousMode = null; + Boolean reset = true; + try { + if (session != null) { + previousMode = session.getHibernateFlushMode(); + session.setHibernateFlushMode(org.hibernate.FlushMode.valueOf(flushMode.name())); + } + try { + reset = callable.call(); + } catch (Exception e) { + reset = false; + } + } + finally { + if (session != null && previousMode != null && reset) { + session.setHibernateFlushMode(previousMode); + } + } + } + + @Override + public org.hibernate.Session openSession() { + org.hibernate.Session session = this.sessionFactory.openSession(); + session.setHibernateFlushMode(org.hibernate.FlushMode.valueOf(defaultFlushModeName)); + return session; + } + + @Override + public Session getCurrentSession() throws ConnectionNotFoundException { + // HibernateSession, just a thin wrapper around default session handling so simply return a new instance here + return new HibernateSession(this, sessionFactory, getDefaultFlushMode()); + } + + @Override + public void destroy() { + try { + super.destroy(); + } finally { + GrailsDomainBinder.clearMappingCache(); + try { + this.gormEnhancer.close(); + } catch (IOException e) { + LOG.error("There was an error shutting down GORM enhancer", e); + } + } + } + + @Override + public void addTenantForSchema(String schemaName) { + addTenantForSchemaInternal(schemaName); + registerAllEntitiesWithEnhancer(); + HibernateConnectionSource defaultConnectionSource = (HibernateConnectionSource) connectionSources.getDefaultConnectionSource(); + DataSource dataSource = defaultConnectionSource.getDataSource(); + if (dataSource instanceof TransactionAwareDataSourceProxy) { + dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } + Object existing = TransactionSynchronizationManager.getResource(dataSource); + if (existing instanceof ConnectionHolder) { + ConnectionHolder connectionHolder = (ConnectionHolder) existing; + Connection connection = connectionHolder.getConnection(); + try { + if (!connection.isClosed() && !connection.isReadOnly()) { + schemaHandler.useDefaultSchema(connection); + } + } catch (SQLException e) { + throw new DatastoreConfigurationException("Failed to reset to default schema: " + e.getMessage(), e); + } + } + + } + + public Metadata getMetadata() { + return metadata; + } + + protected void registerAllEntitiesWithEnhancer() { + for (PersistentEntity persistentEntity : mappingContext.getPersistentEntities()) { + gormEnhancer.registerEntity(persistentEntity); + } + } + + private void addTenantForSchemaInternal(final String schemaName) { + if (multiTenantMode != MultiTenancySettings.MultiTenancyMode.SCHEMA) { + throw new ConfigurationException("The method [addTenantForSchema] can only be called with multi-tenancy mode SCHEMA. Current mode is: " + multiTenantMode); + } + HibernateConnectionSourceFactory factory = (HibernateConnectionSourceFactory) connectionSources.getFactory(); + HibernateConnectionSource defaultConnectionSource = (HibernateConnectionSource) connectionSources.getDefaultConnectionSource(); + HibernateConnectionSourceSettings tenantSettings; + try { + tenantSettings = (HibernateConnectionSourceSettings) connectionSources.getDefaultConnectionSource().getSettings().clone(); + } catch (CloneNotSupportedException e) { + throw new ConfigurationException("Couldn't clone default Hibernate settings! " + e.getMessage(), e); + } + tenantSettings.getHibernate().put(Environment.DEFAULT_SCHEMA, schemaName); + + String dbCreate = tenantSettings.getDataSource().getDbCreate(); + + SchemaAutoTooling schemaAutoTooling = dbCreate != null ? SchemaAutoTooling.interpret(dbCreate) : null; + if (schemaAutoTooling != null && schemaAutoTooling != SchemaAutoTooling.VALIDATE && schemaAutoTooling != SchemaAutoTooling.NONE) { + + Connection connection = null; + try { + connection = defaultConnectionSource.getDataSource().getConnection(); + try { + schemaHandler.useSchema(connection, schemaName); + } catch (Exception e) { + // schema doesn't exist + schemaHandler.createSchema(connection, schemaName); + } + + } catch (SQLException e) { + throw new DatastoreConfigurationException(String.format("Failed to create schema for name [%s]", schemaName)); + } + finally { + if (connection != null) { + try { + schemaHandler.useDefaultSchema(connection); + connection.close(); + } catch (SQLException e) { + //ignore + } + } + } + } + + DataSource dataSource = defaultConnectionSource.getDataSource(); + dataSource = new MultiTenantDataSource(dataSource, schemaName) { + @Override + public Connection getConnection() throws SQLException { + Connection connection = super.getConnection(); + schemaHandler.useSchema(connection, schemaName); + return new MultiTenantConnection(connection, schemaHandler); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + Connection connection = super.getConnection(username, password); + schemaHandler.useSchema(connection, schemaName); + return new MultiTenantConnection(connection, schemaHandler); + } + }; + DefaultConnectionSource dataSourceConnectionSource = new DefaultConnectionSource<>(schemaName, dataSource, tenantSettings.getDataSource()); + ConnectionSource connectionSource = factory.create(schemaName, dataSourceConnectionSource, tenantSettings); + SingletonConnectionSources singletonConnectionSources = new SingletonConnectionSources<>(connectionSource, connectionSources.getBaseConfiguration()); + HibernateDatastore childDatastore = new HibernateDatastore(singletonConnectionSources, (HibernateMappingContext) mappingContext, eventPublisher) { + @Override + protected HibernateGormEnhancer initialize() { + return null; + } + }; + datastoresByConnectionSource.put(connectionSource.getName(), childDatastore); + } + + private Metadata getMetadataInternal() { + Metadata metadata = null; + ServiceRegistry bootstrapServiceRegistry = ((SessionFactoryImplementor) sessionFactory).getServiceRegistry().getParentServiceRegistry(); + Iterable integrators = bootstrapServiceRegistry.getService(IntegratorService.class).getIntegrators(); + for (Integrator integrator : integrators) { + if (integrator instanceof MetadataIntegrator) { + metadata = ((MetadataIntegrator) integrator).getMetadata(); + } + } + return metadata; + } + + private static HibernateConnectionSourceFactory createConnectionFactoryForDataSource(final DataSource dataSource, Class... classes) { + HibernateConnectionSourceFactory hibernateConnectionSourceFactory = new HibernateConnectionSourceFactory(classes); + hibernateConnectionSourceFactory.setDataSourceConnectionSourceFactory( + new DataSourceConnectionSourceFactory() { + @Override + public ConnectionSource create(String name, DataSourceSettings settings) { + if (ConnectionSource.DEFAULT.equals(name)) { + return new DataSourceConnectionSource(ConnectionSource.DEFAULT, dataSource, settings); + } + else { + return super.create(name, settings); + } + } + } + ); + return hibernateConnectionSourceFactory; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateEventListeners.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateEventListeners.java new file mode 100644 index 00000000000..ee1ef72940a --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateEventListeners.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import java.util.Map; + +public class HibernateEventListeners { + + private Map listenerMap; + + public Map getListenerMap() { + return listenerMap; + } + + public void setListenerMap(Map listenerMap) { + this.listenerMap = listenerMap; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy new file mode 100644 index 00000000000..9a47fb8c419 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import groovy.transform.CompileStatic + +import org.springframework.transaction.PlatformTransactionManager + +import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormInstanceApi +import org.grails.datastore.gorm.GormStaticApi +import org.grails.datastore.gorm.GormValidationApi +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings + +/** + * Extended GORM Enhancer that fills out the remaining GORM for Hibernate methods + * and implements string-based query support via HQL. + * + * @author Graeme Rocher + * @since 1.0 + */ +@CompileStatic +class HibernateGormEnhancer extends GormEnhancer { + + @Deprecated + HibernateGormEnhancer(HibernateDatastore datastore, PlatformTransactionManager transactionManager) { + super(datastore, transactionManager) + } + + HibernateGormEnhancer(Datastore datastore, PlatformTransactionManager transactionManager, ConnectionSourceSettings settings) { + super(datastore, transactionManager, settings) + } + + @Override + protected GormStaticApi getStaticApi(Class cls, String qualifier) { + HibernateDatastore hibernateDatastore = (HibernateDatastore) datastore + HibernateDatastore datastoreForConnection = hibernateDatastore.getDatastoreForConnection(qualifier) + new HibernateGormStaticApi( + cls, + datastoreForConnection, + createDynamicFinders(datastoreForConnection), + Thread.currentThread().contextClassLoader, + datastoreForConnection.getTransactionManager() + ) + } + + @Override + protected GormInstanceApi getInstanceApi(Class cls, String qualifier) { + HibernateDatastore hibernateDatastore = (HibernateDatastore) datastore + new HibernateGormInstanceApi(cls, hibernateDatastore.getDatastoreForConnection(qualifier), Thread.currentThread().contextClassLoader) + } + + @Override + protected GormValidationApi getValidationApi(Class cls, String qualifier) { + HibernateDatastore hibernateDatastore = (HibernateDatastore) datastore + new HibernateGormValidationApi(cls, hibernateDatastore.getDatastoreForConnection(qualifier), Thread.currentThread().contextClassLoader) + } + + @Override + protected void registerConstraints(Datastore datastore) { + // no-op + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy new file mode 100644 index 00000000000..34385e2de57 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import org.hibernate.engine.spi.EntityEntry +import org.hibernate.engine.spi.SessionImplementor +import org.hibernate.persister.entity.EntityPersister +import org.hibernate.tuple.NonIdentifierAttribute + +import org.grails.orm.hibernate.cfg.GrailsHibernateUtil + +/** + * The implementation of the GORM instance API contract for Hibernate. + * + * @author Graeme Rocher + * @since 1.0 + */ +@CompileStatic +class HibernateGormInstanceApi extends AbstractHibernateGormInstanceApi { + + protected InstanceApiHelper instanceApiHelper + + HibernateGormInstanceApi(Class persistentClass, HibernateDatastore datastore, ClassLoader classLoader) { + super(persistentClass, datastore, classLoader, null) + hibernateTemplate = new GrailsHibernateTemplate(sessionFactory, datastore) + instanceApiHelper = new InstanceApiHelper((GrailsHibernateTemplate) hibernateTemplate) + } + + /** + * Checks whether a field is dirty + * + * @param instance The instance + * @param fieldName The name of the field + * + * @return true if the field is dirty + */ + + @CompileDynamic + boolean isDirty(D instance, String fieldName) { + SessionImplementor session = (SessionImplementor) sessionFactory.currentSession + def entry = findEntityEntry(instance, session) + if (!entry || !entry.loadedState) { + return false + } + + EntityPersister persister = entry.persister + Object[] values = persister.getPropertyValues(instance) + def dirtyProperties = findDirty(persister, values, entry, instance, session) + if (dirtyProperties == null) { + return false + } + else { + int fieldIndex = persister.getEntityMetamodel().getProperties().findIndexOf { NonIdentifierAttribute attribute -> fieldName == attribute.name } + return fieldIndex in dirtyProperties + } + } + + @CompileDynamic // required for Hibernate 5.2 compatibility + private def findDirty(EntityPersister persister, Object[] values, EntityEntry entry, D instance, SessionImplementor session) { + persister.findDirty(values, entry.loadedState, instance, session) + } + + /** + * Checks whether an entity is dirty + * + * @param instance The instance + * @return true if it is dirty + */ + @CompileDynamic + boolean isDirty(D instance) { + SessionImplementor session = (SessionImplementor) sessionFactory.currentSession + def entry = findEntityEntry(instance, session) + if (!entry || !entry.loadedState) { + return false + } + EntityPersister persister = entry.persister + Object[] currentState = persister.getPropertyValues(instance) + def dirtyPropertyIndexes = findDirty(persister, currentState, entry, instance, session) + return dirtyPropertyIndexes != null + } + + /** + * Obtains a list of property names that are dirty + * + * @param instance The instance + * @return A list of property names that are dirty + */ + + @CompileDynamic + List getDirtyPropertyNames(D instance) { + SessionImplementor session = (SessionImplementor) sessionFactory.currentSession + def entry = findEntityEntry(instance, session) + if (!entry || !entry.loadedState) { + return [] + } + + EntityPersister persister = entry.persister + Object[] currentState = persister.getPropertyValues(instance) + int[] dirtyPropertyIndexes = findDirty(persister, currentState, entry, instance, session) + List names = [] + def entityProperties = persister.getEntityMetamodel().getProperties() + for (index in dirtyPropertyIndexes) { + names.add(entityProperties[index].name) + } + return names + } + + /** + * Gets the original persisted value of a field. + * + * @param fieldName The field name + * @return The original persisted value + */ + Object getPersistentValue(D instance, String fieldName) { + SessionImplementor session = (SessionImplementor) sessionFactory.currentSession + def entry = findEntityEntry(instance, session, false) + if (!entry || !entry.loadedState) { + return null + } + + EntityPersister persister = entry.persister + int fieldIndex = persister.getEntityMetamodel().getProperties().findIndexOf { + NonIdentifierAttribute attribute -> fieldName == attribute.name + } + return fieldIndex == -1 ? null : entry.loadedState[fieldIndex] + } + + protected EntityEntry findEntityEntry(D instance, SessionImplementor session, boolean forDirtyCheck = true) { + def entry = session.persistenceContext.getEntry(instance) + if (!entry) { + return null + } + + if (forDirtyCheck && !entry.requiresDirtyCheck(instance) && entry.loadedState) { + return null + } + + return entry + } + + @Override + void setObjectToReadWrite(Object target) { + GrailsHibernateUtil.setObjectToReadWrite(target, sessionFactory) + } + + @Override + void setObjectToReadOnly(Object target) { + GrailsHibernateUtil.setObjectToReadyOnly(target, sessionFactory) + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy new file mode 100644 index 00000000000..33724ab93ee --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import jakarta.persistence.FlushModeType +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery +import jakarta.persistence.criteria.Root + +import org.hibernate.Criteria +import org.hibernate.FlushMode +import org.hibernate.LockMode +import org.hibernate.Session +import org.hibernate.SessionFactory +import org.hibernate.query.Query + +import org.springframework.core.convert.ConversionService +import org.grails.orm.hibernate.support.hibernate5.SessionHolder +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.support.TransactionSynchronizationManager + +import grails.orm.HibernateCriteriaBuilder +import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.finders.DynamicFinder +import org.grails.datastore.gorm.finders.FinderMethod +import org.grails.datastore.mapping.query.api.BuildableCriteria as GrailsCriteria +import org.grails.datastore.mapping.query.event.PostQueryEvent +import org.grails.datastore.mapping.query.event.PreQueryEvent +import org.grails.orm.hibernate.exceptions.GrailsQueryException +import org.grails.orm.hibernate.query.GrailsHibernateQueryUtils +import org.grails.orm.hibernate.query.HibernateHqlQuery +import org.grails.orm.hibernate.query.HibernateQuery +import org.grails.orm.hibernate.query.PagedResultList + +/** + * The implementation of the GORM static method contract for Hibernate + * + * @author Graeme Rocher + * @since 1.0 + */ +@CompileStatic +class HibernateGormStaticApi extends AbstractHibernateGormStaticApi { + + protected SessionFactory sessionFactory + protected ConversionService conversionService + protected Class identityType + protected ClassLoader classLoader + private HibernateGormInstanceApi instanceApi + private int defaultFlushMode + + HibernateGormStaticApi(Class persistentClass, HibernateDatastore datastore, List finders, + ClassLoader classLoader, PlatformTransactionManager transactionManager) { + super(persistentClass, datastore, finders, transactionManager) + this.classLoader = classLoader + sessionFactory = datastore.getSessionFactory() + conversionService = datastore.mappingContext.conversionService + + identityType = persistentEntity.identity?.type + this.defaultFlushMode = datastore.getDefaultFlushMode() + instanceApi = new HibernateGormInstanceApi<>(persistentClass, datastore, classLoader) + } + + @Override + GrailsHibernateTemplate getHibernateTemplate() { + return (GrailsHibernateTemplate) super.getHibernateTemplate() + } + + @Override + List list(Map params = Collections.emptyMap()) { + hibernateTemplate.execute { Session session -> + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder() + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(persistentEntity.javaClass) + Root queryRoot = criteriaQuery.from(persistentEntity.javaClass) + GrailsHibernateQueryUtils.populateArgumentsForCriteria( + persistentEntity, + criteriaQuery, + queryRoot, + criteriaBuilder, + params, + datastore.mappingContext.conversionService, + true + ) + Query query = session.createQuery(criteriaQuery) + + GrailsHibernateQueryUtils.populateArgumentsForCriteria( + persistentEntity, + query, + params, + datastore.mappingContext.conversionService, + true + ) + + HibernateHqlQuery hibernateQuery = new HibernateHqlQuery( + new HibernateSession((HibernateDatastore) datastore, sessionFactory), + persistentEntity, + query + ) + hibernateTemplate.applySettings(query) + + params = params ? new HashMap(params) : Collections.emptyMap() + if (params.containsKey(DynamicFinder.ARGUMENT_MAX)) { + return new PagedResultList( + hibernateTemplate, + persistentEntity, + hibernateQuery, + criteriaQuery, + queryRoot, + criteriaBuilder + ) + } + else { + return hibernateQuery.list() + } + } + } + + @Override + def propertyMissing(String name) { + return GormEnhancer.findStaticApi(persistentClass, name) + } + + @Override + GrailsCriteria createCriteria() { + def builder = new HibernateCriteriaBuilder(persistentClass, sessionFactory) + builder.datastore = (AbstractHibernateDatastore) datastore + builder.conversionService = conversionService + return builder + } + + @Override + D lock(Serializable id) { + (D) hibernateTemplate.lock((Class)persistentClass, convertIdentifier(id), LockMode.PESSIMISTIC_WRITE) + } + + @Override + Integer executeUpdate(CharSequence query, Map params, Map args) { + + if (query instanceof GString) { + params = new LinkedHashMap(params) + query = buildNamedParameterQueryFromGString((GString) query, params) + } + + def template = hibernateTemplate + SessionFactory sessionFactory = this.sessionFactory + return (Integer) template.execute { Session session -> + Query q = (Query) session.createQuery(query.toString()) + template.applySettings(q) + def sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory) + if (sessionHolder && sessionHolder.hasTimeout()) { + q.timeout = sessionHolder.timeToLiveInSeconds + } + + populateQueryArguments(q, params) + populateQueryArguments(q, args) + populateQueryWithNamedArguments(q, params) + + return withQueryEvents(q) { + q.executeUpdate() + } + } + } + + @Override + Integer executeUpdate(CharSequence query, Collection params, Map args) { + if (query instanceof GString) { + throw new GrailsQueryException("Unsafe query [$query]. GORM cannot automatically escape a GString value when combined with ordinal parameters, so this query is potentially vulnerable to HQL injection attacks. Please embed the parameters within the GString so they can be safely escaped.") + } + + def template = hibernateTemplate + SessionFactory sessionFactory = this.sessionFactory + + return (Integer) template.execute { Session session -> + Query q = (Query) session.createQuery(query.toString()) + template.applySettings(q) + def sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory) + if (sessionHolder && sessionHolder.hasTimeout()) { + q.timeout = sessionHolder.timeToLiveInSeconds + } + + params.eachWithIndex { val, int i -> + if (val instanceof CharSequence) { + q.setParameter(i, val.toString()) + } + else { + q.setParameter(i, val) + } + } + populateQueryArguments(q, args) + return withQueryEvents(q) { + q.executeUpdate() + } + } + } + + protected T withQueryEvents(Query query, Closure callable) { + HibernateDatastore hibernateDatastore = (HibernateDatastore) datastore + + def eventPublisher = hibernateDatastore.applicationEventPublisher + + def hqlQuery = new HibernateHqlQuery(new HibernateSession(hibernateDatastore, sessionFactory), persistentEntity, query) + eventPublisher.publishEvent(new PreQueryEvent(hibernateDatastore, hqlQuery)) + + def result = callable.call() + + eventPublisher.publishEvent(new PostQueryEvent(hibernateDatastore, hqlQuery, Collections.singletonList(result))) + return result + } + + @Override + protected void firePostQueryEvent(Session session, Criteria criteria, Object result) { + if (result instanceof List) { + datastore.applicationEventPublisher.publishEvent(new PostQueryEvent(datastore, new HibernateQuery(criteria, persistentEntity), (List) result)) + } + else { + datastore.applicationEventPublisher.publishEvent(new PostQueryEvent(datastore, new HibernateQuery(criteria, persistentEntity), Collections.singletonList(result))) + } + } + + @Override + protected void firePreQueryEvent(Session session, Criteria criteria) { + datastore.applicationEventPublisher.publishEvent(new PreQueryEvent(datastore, new HibernateQuery(criteria, persistentEntity))) + } + + @Override + protected HibernateHqlQuery createHqlQuery(Session session, Query q) { + HibernateSession hibernateSession = new HibernateSession((HibernateDatastore) datastore, sessionFactory) + FlushMode hibernateMode = session.getHibernateFlushMode() + switch (hibernateMode) { + case FlushMode.AUTO: + hibernateSession.setFlushMode(FlushModeType.AUTO) + break + case FlushMode.ALWAYS: + hibernateSession.setFlushMode(FlushModeType.AUTO) + break + default: + hibernateSession.setFlushMode(FlushModeType.COMMIT) + + } + HibernateHqlQuery query = new HibernateHqlQuery(hibernateSession, persistentEntity, q) + return query + } + + @CompileDynamic + protected void setResultTransformer(Criteria c) { + c.resultTransformer = Criteria.DISTINCT_ROOT_ENTITY + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy new file mode 100644 index 00000000000..61db55a4551 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import groovy.transform.CompileStatic + +import org.hibernate.FlushMode +import org.hibernate.Session + +@CompileStatic +class HibernateGormValidationApi extends AbstractHibernateGormValidationApi { + + HibernateGormValidationApi(Class persistentClass, HibernateDatastore datastore, ClassLoader classLoader) { + super(persistentClass, datastore, classLoader) + hibernateTemplate = new GrailsHibernateTemplate(datastore.getSessionFactory(), datastore) + } + + @Override + void restoreFlushMode(Session session, Object previousFlushMode) { + if (previousFlushMode != null) { + session.setHibernateFlushMode((FlushMode) previousFlushMode) + } + } + + @Override + Object readPreviousFlushMode(Session session) { + return session.getHibernateFlushMode() + } + + @Override + def applyManualFlush(Session session) { + session.setHibernateFlushMode(FlushMode.MANUAL) + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateMappingContextSessionFactoryBean.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateMappingContextSessionFactoryBean.java new file mode 100644 index 00000000000..29e969187a8 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateMappingContextSessionFactoryBean.java @@ -0,0 +1,567 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate; + +import java.io.File; +import java.util.Map; +import java.util.Properties; + +import javax.naming.NameNotFoundException; +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.HibernateException; +import org.hibernate.Interceptor; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.Environment; +import org.hibernate.cfg.NamingStrategy; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.util.Assert; + +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.orm.hibernate.cfg.HibernateMappingContext; +import org.grails.orm.hibernate.cfg.HibernateMappingContextConfiguration; +import org.grails.orm.hibernate.support.hibernate5.HibernateExceptionTranslator; + +/** + * Configures a SessionFactory using a {@link org.grails.orm.hibernate.cfg.HibernateMappingContext} and a {@link org.grails.orm.hibernate.cfg.HibernateMappingContextConfiguration} + * + * @author Graeme Rocher + * @since 5.0 + */ +public class HibernateMappingContextSessionFactoryBean extends HibernateExceptionTranslator + implements FactoryBean, ResourceLoaderAware, DisposableBean, + ApplicationContextAware, InitializingBean, BeanClassLoaderAware { + protected Class configClass = HibernateMappingContextConfiguration.class; + protected HibernateMappingContext hibernateMappingContext; + protected PlatformTransactionManager transactionManager; + + private DataSource dataSource; + private Resource[] configLocations; + private String[] mappingResources; + private Resource[] mappingLocations; + private Resource[] cacheableMappingLocations; + private Resource[] mappingJarLocations; + private Resource[] mappingDirectoryLocations; + private Interceptor entityInterceptor; + private NamingStrategy namingStrategy; + private Properties hibernateProperties; + private Class[] annotatedClasses; + private String[] annotatedPackages; + private String[] packagesToScan; + private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + private HibernateMappingContextConfiguration configuration; + private SessionFactory sessionFactory; + + private static final Log LOG = LogFactory.getLog(HibernateMappingContextSessionFactoryBean.class); + protected Class currentSessionContextClass; + protected Map eventListeners; + protected HibernateEventListeners hibernateEventListeners; + protected ApplicationContext applicationContext; + protected boolean proxyIfReloadEnabled = false; + protected String sessionFactoryBeanName = "sessionFactory"; + protected String dataSourceName = ConnectionSource.DEFAULT; + protected ClassLoader classLoader; + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + public void afterPropertiesSet() throws Exception { + Thread thread = Thread.currentThread(); + ClassLoader cl = thread.getContextClassLoader(); + try { + thread.setContextClassLoader(classLoader); + buildSessionFactory(); + } + finally { + thread.setContextClassLoader(cl); + } + } + + public PlatformTransactionManager getTransactionManager() { + return transactionManager; + } + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + public void setHibernateMappingContext(HibernateMappingContext hibernateMappingContext) { + this.hibernateMappingContext = hibernateMappingContext; + } + + /** + * Sets the class to be used for Hibernate Configuration. + * @param configClass A subclass of the Hibernate Configuration class + */ + public void setConfigClass(Class configClass) { + this.configClass = configClass; + } + + /** + * Set the DataSource to be used by the SessionFactory. + * If set, this will override corresponding settings in Hibernate properties. + *

If this is set, the Hibernate settings should not define + * a connection provider to avoid meaningless double configuration. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + public DataSource getDataSource() { + return dataSource; + } + + /** + * Set the location of a single Hibernate XML config file, for example as + * classpath resource "classpath:hibernate.cfg.xml". + *

Note: Can be omitted when all necessary properties and mapping + * resources are specified locally via this bean. + * @see org.hibernate.cfg.Configuration#configure(java.net.URL) + */ + public void setConfigLocation(Resource configLocation) { + configLocations = new Resource[] {configLocation}; + } + + /** + * Set the locations of multiple Hibernate XML config files, for example as + * classpath resources "classpath:hibernate.cfg.xml,classpath:extension.cfg.xml". + *

Note: Can be omitted when all necessary properties and mapping + * resources are specified locally via this bean. + * @see org.hibernate.cfg.Configuration#configure(java.net.URL) + */ + public void setConfigLocations(Resource[] configLocations) { + this.configLocations = configLocations; + } + + public Resource[] getConfigLocations() { + return configLocations; + } + + /** + * Set Hibernate mapping resources to be found in the class path, + * like "example.hbm.xml" or "mypackage/example.hbm.xml". + * Analogous to mapping entries in a Hibernate XML config file. + * Alternative to the more generic setMappingLocations method. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see #setMappingLocations + * @see org.hibernate.cfg.Configuration#addResource + */ + public void setMappingResources(String[] mappingResources) { + this.mappingResources = mappingResources; + } + + public String[] getMappingResources() { + return mappingResources; + } + + /** + * Set locations of Hibernate mapping files, for example as classpath + * resource "classpath:example.hbm.xml". Supports any resource location + * via Spring's resource abstraction, for example relative paths like + * "WEB-INF/mappings/example.hbm.xml" when running in an application context. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addInputStream + */ + public void setMappingLocations(Resource[] mappingLocations) { + this.mappingLocations = mappingLocations; + } + + public Resource[] getMappingLocations() { + return mappingLocations; + } + + /** + * Set locations of cacheable Hibernate mapping files, for example as web app + * resource "/WEB-INF/mapping/example.hbm.xml". Supports any resource location + * via Spring's resource abstraction, as long as the resource can be resolved + * in the file system. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addCacheableFile(java.io.File) + */ + public void setCacheableMappingLocations(Resource[] cacheableMappingLocations) { + this.cacheableMappingLocations = cacheableMappingLocations; + } + + public Resource[] getCacheableMappingLocations() { + return cacheableMappingLocations; + } + + /** + * Set locations of jar files that contain Hibernate mapping resources, + * like "WEB-INF/lib/example.hbm.jar". + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addJar(java.io.File) + */ + public void setMappingJarLocations(Resource[] mappingJarLocations) { + this.mappingJarLocations = mappingJarLocations; + } + + public Resource[] getMappingJarLocations() { + return mappingJarLocations; + } + + /** + * Set locations of directories that contain Hibernate mapping resources, + * like "WEB-INF/mappings". + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addDirectory(java.io.File) + */ + public void setMappingDirectoryLocations(Resource[] mappingDirectoryLocations) { + this.mappingDirectoryLocations = mappingDirectoryLocations; + } + + public Resource[] getMappingDirectoryLocations() { + return mappingDirectoryLocations; + } + + /** + * Set a Hibernate entity interceptor that allows to inspect and change + * property values before writing to and reading from the database. + * Will get applied to any new Session created by this factory. + * @see org.hibernate.cfg.Configuration#setInterceptor + */ + public void setEntityInterceptor(Interceptor entityInterceptor) { + this.entityInterceptor = entityInterceptor; + } + + public Interceptor getEntityInterceptor() { + return entityInterceptor; + } + + /** + * Set a Hibernate NamingStrategy for the SessionFactory, determining the + * physical column and table names given the info in the mapping document. + */ + public void setNamingStrategy(NamingStrategy namingStrategy) { + this.namingStrategy = namingStrategy; + } + + public NamingStrategy getNamingStrategy() { + return namingStrategy; + } + + /** + * Set Hibernate properties, such as "hibernate.dialect". + *

Note: Do not specify a transaction provider here when using + * Spring-driven transactions. It is also advisable to omit connection + * provider settings and use a Spring-set DataSource instead. + * @see #setDataSource + */ + public void setHibernateProperties(Properties hibernateProperties) { + this.hibernateProperties = hibernateProperties; + } + + /** + * Return the Hibernate properties, if any. Mainly available for + * configuration through property paths that specify individual keys. + */ + public Properties getHibernateProperties() { + if (hibernateProperties == null) { + hibernateProperties = new Properties(); + } + return hibernateProperties; + } + + /** + * Specify annotated entity classes to register with this Hibernate SessionFactory. + * @see org.hibernate.cfg.Configuration#addAnnotatedClass(Class) + */ + public void setAnnotatedClasses(Class[] annotatedClasses) { + this.annotatedClasses = annotatedClasses; + } + + public Class[] getAnnotatedClasses() { + return annotatedClasses; + } + + /** + * Specify the names of annotated packages, for which package-level + * annotation metadata will be read. + * @see org.hibernate.cfg.Configuration#addPackage(String) + */ + public void setAnnotatedPackages(String[] annotatedPackages) { + this.annotatedPackages = annotatedPackages; + } + + public String[] getAnnotatedPackages() { + return annotatedPackages; + } + + /** + * Specify packages to search for autodetection of your entity classes in the + * classpath. This is analogous to Spring's component-scan feature + * ({@link org.springframework.context.annotation.ClassPathBeanDefinitionScanner}). + */ + public void setPackagesToScan(String... packagesToScan) { + this.packagesToScan = packagesToScan; + } + + public String[] getPackagesToScan() { + return packagesToScan; + } + + public void setResourceLoader(ResourceLoader resourceLoader) { + resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + } + + /** + * @param proxyIfReloadEnabled Sets whether a proxy should be created if reload is enabled + */ + public void setProxyIfReloadEnabled(boolean proxyIfReloadEnabled) { + this.proxyIfReloadEnabled = proxyIfReloadEnabled; + } + + public boolean isProxyIfReloadEnabled() { + return proxyIfReloadEnabled; + } + + /** + * Sets class to be used for the Hibernate CurrentSessionContext. + * + * @param currentSessionContextClass An implementation of the CurrentSessionContext interface + */ + public void setCurrentSessionContextClass(Class currentSessionContextClass) { + this.currentSessionContextClass = currentSessionContextClass; + } + + public Class getCurrentSessionContextClass() { + return currentSessionContextClass; + } + + public Class getConfigClass() { + return configClass; + } + + public void setHibernateEventListeners(final HibernateEventListeners listeners) { + hibernateEventListeners = listeners; + } + + public HibernateEventListeners getHibernateEventListeners() { + return hibernateEventListeners; + } + + public void setSessionFactoryBeanName(String name) { + sessionFactoryBeanName = name; + } + + public String getSessionFactoryBeanName() { + return sessionFactoryBeanName; + } + + public void setDataSourceName(String name) { + dataSourceName = name; + } + + public String getDataSourceName() { + return dataSourceName; + } + + /** + * Specify the Hibernate event listeners to register, with listener types + * as keys and listener objects as values. Instead of a single listener object, + * you can also pass in a list or set of listeners objects as value. + *

See the Hibernate documentation for further details on listener types + * and associated listener interfaces. + * @param eventListeners Map with listener type Strings as keys and + * listener objects as values + */ + public void setEventListeners(Map eventListeners) { + this.eventListeners = eventListeners; + } + + public Map getEventListeners() { + return eventListeners; + } + + protected void buildSessionFactory() throws Exception { + + configuration = newConfiguration(); + + if (hibernateMappingContext == null) { + + throw new IllegalArgumentException("HibernateMappingContext is required."); + } + + configuration.setHibernateMappingContext(hibernateMappingContext); + + if (configLocations != null) { + for (Resource resource : configLocations) { + // Load Hibernate configuration from given location. + configuration.configure(resource.getURL()); + } + } + + if (mappingResources != null) { + // Register given Hibernate mapping definitions, contained in resource files. + for (String mapping : mappingResources) { + Resource mr = new ClassPathResource(mapping.trim(), resourcePatternResolver.getClassLoader()); + configuration.addInputStream(mr.getInputStream()); + } + } + + if (mappingLocations != null) { + // Register given Hibernate mapping definitions, contained in resource files. + for (Resource resource : mappingLocations) { + configuration.addInputStream(resource.getInputStream()); + } + } + + if (cacheableMappingLocations != null) { + // Register given cacheable Hibernate mapping definitions, read from the file system. + for (Resource resource : cacheableMappingLocations) { + configuration.addCacheableFile(resource.getFile()); + } + } + + if (mappingJarLocations != null) { + // Register given Hibernate mapping definitions, contained in jar files. + for (Resource resource : mappingJarLocations) { + configuration.addJar(resource.getFile()); + } + } + + if (mappingDirectoryLocations != null) { + // Register all Hibernate mapping definitions in the given directories. + for (Resource resource : mappingDirectoryLocations) { + File file = resource.getFile(); + if (!file.isDirectory()) { + throw new IllegalArgumentException("Mapping directory location [" + resource + "] does not denote a directory"); + } + configuration.addDirectory(file); + } + } + + if (entityInterceptor != null) { + configuration.setInterceptor(entityInterceptor); + } + + if (namingStrategy != null) { + // configuration.setNamingStrategy(namingStrategy); + } + + if (hibernateProperties != null) { + configuration.addProperties(hibernateProperties); + } + + if (annotatedClasses != null) { + configuration.addAnnotatedClasses(annotatedClasses); + } + + if (annotatedPackages != null) { + configuration.addPackages(annotatedPackages); + } + + if (packagesToScan != null) { + configuration.scanPackages(packagesToScan); + } + + if (eventListeners != null) { + configuration.setEventListeners(eventListeners); + } + + sessionFactory = doBuildSessionFactory(); + } + + protected SessionFactory doBuildSessionFactory() { + return configuration.buildSessionFactory(); + } + + /** + * Return the Hibernate Configuration object used to build the SessionFactory. + * Allows for access to configuration metadata stored there (rarely needed). + * @throws IllegalStateException if the Configuration object has not been initialized yet + */ + public final Configuration getConfiguration() { + Assert.state(configuration != null, "Configuration not initialized yet"); + return configuration; + } + + public SessionFactory getObject() { + return sessionFactory; + } + + public Class getObjectType() { + return sessionFactory == null ? SessionFactory.class : sessionFactory.getClass(); + } + + public boolean isSingleton() { + return true; + } + + public void destroy() { + try { + sessionFactory.close(); + } + catch (HibernateException e) { + if (e.getCause() instanceof NameNotFoundException) { + LOG.debug(e.getCause().getMessage(), e); + } + else { + throw e; + } + } + } + + protected HibernateMappingContextConfiguration newConfiguration() throws Exception { + if (configClass == null) { + configClass = HibernateMappingContextConfiguration.class; + } + HibernateMappingContextConfiguration config = BeanUtils.instantiateClass(configClass); + config.setDataSourceName(dataSourceName); + config.setApplicationContext(applicationContext); + config.setSessionFactoryBeanName(sessionFactoryBeanName); + config.setHibernateEventListeners(hibernateEventListeners); + if (currentSessionContextClass != null) { + config.setProperty(Environment.CURRENT_SESSION_CONTEXT_CLASS, currentSessionContextClass.getName()); + } + return config; + } + + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java new file mode 100644 index 00000000000..a2b8570306c --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import jakarta.persistence.FlushModeType; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import org.hibernate.Criteria; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.proxy.HibernateProxy; + +import org.springframework.context.ApplicationEventPublisher; + +import org.grails.datastore.gorm.timestamp.DefaultTimestampProvider; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.config.GormProperties; +import org.grails.datastore.mapping.proxy.ProxyHandler; +import org.grails.datastore.mapping.query.Query; +import org.grails.datastore.mapping.query.api.QueryableCriteria; +import org.grails.datastore.mapping.query.event.PostQueryEvent; +import org.grails.datastore.mapping.query.event.PreQueryEvent; +import org.grails.datastore.mapping.query.jpa.JpaQueryBuilder; +import org.grails.datastore.mapping.query.jpa.JpaQueryInfo; +import org.grails.datastore.mapping.reflect.ClassPropertyFetcher; +import org.grails.orm.hibernate.proxy.HibernateProxyHandler; +import org.grails.orm.hibernate.query.HibernateHqlQuery; +import org.grails.orm.hibernate.query.HibernateQuery; + +/** + * Session implementation that wraps a Hibernate {@link org.hibernate.Session}. + * + * @author Graeme Rocher + * @since 1.0 + */ +@SuppressWarnings("rawtypes") +public class HibernateSession extends AbstractHibernateSession { + + ProxyHandler proxyHandler = new HibernateProxyHandler(); + DefaultTimestampProvider timestampProvider; + + public HibernateSession(HibernateDatastore hibernateDatastore, SessionFactory sessionFactory, int defaultFlushMode) { + super(hibernateDatastore, sessionFactory); + + hibernateTemplate = new GrailsHibernateTemplate(sessionFactory, (HibernateDatastore) getDatastore()); + } + + public HibernateSession(HibernateDatastore hibernateDatastore, SessionFactory sessionFactory) { + this(hibernateDatastore, sessionFactory, hibernateDatastore.getDefaultFlushMode()); + } + + @Override + public Serializable getObjectIdentifier(Object instance) { + if (instance == null) return null; + if (proxyHandler.isProxy(instance)) { + return ((HibernateProxy) instance).getHibernateLazyInitializer().getIdentifier(); + } + Class type = instance.getClass(); + ClassPropertyFetcher cpf = ClassPropertyFetcher.forClass(type); + final PersistentEntity persistentEntity = getMappingContext().getPersistentEntity(type.getName()); + if (persistentEntity != null) { + return (Serializable) cpf.getPropertyValue(instance, persistentEntity.getIdentity().getName()); + } + return null; + } + + /** + * Deletes all objects matching the given criteria. + * + * @param criteria The criteria + * @return The total number of records deleted + */ + public long deleteAll(final QueryableCriteria criteria) { + return getHibernateTemplate().execute((GrailsHibernateTemplate.HibernateCallback) session -> { + JpaQueryBuilder builder = new JpaQueryBuilder(criteria); + builder.setConversionService(getMappingContext().getConversionService()); + builder.setHibernateCompatible(true); + JpaQueryInfo jpaQueryInfo = builder.buildDelete(); + + org.hibernate.query.Query query = session.createQuery(jpaQueryInfo.getQuery()); + getHibernateTemplate().applySettings(query); + + List parameters = jpaQueryInfo.getParameters(); + if (parameters != null) { + for (int i = 0, count = parameters.size(); i < count; i++) { + query.setParameter(JpaQueryBuilder.PARAMETER_NAME_PREFIX + (i + 1), parameters.get(i)); + } + } + + HibernateHqlQuery hqlQuery = new HibernateHqlQuery(HibernateSession.this, criteria.getPersistentEntity(), query); + ApplicationEventPublisher applicationEventPublisher = datastore.getApplicationEventPublisher(); + applicationEventPublisher.publishEvent(new PreQueryEvent(datastore, hqlQuery)); + int result = query.executeUpdate(); + applicationEventPublisher.publishEvent(new PostQueryEvent(datastore, hqlQuery, Collections.singletonList(result))); + return result; + }); + } + + /** + * Updates all objects matching the given criteria and property values. + * + * @param criteria The criteria + * @param properties The properties + * @return The total number of records updated + */ + public long updateAll(final QueryableCriteria criteria, final Map properties) { + return getHibernateTemplate().execute((GrailsHibernateTemplate.HibernateCallback) session -> { + JpaQueryBuilder builder = new JpaQueryBuilder(criteria); + builder.setConversionService(getMappingContext().getConversionService()); + builder.setHibernateCompatible(true); + PersistentEntity targetEntity = criteria.getPersistentEntity(); + PersistentProperty lastUpdated = targetEntity.getPropertyByName(GormProperties.LAST_UPDATED); + if (lastUpdated != null && targetEntity.getMapping().getMappedForm().isAutoTimestamp()) { + if (timestampProvider == null) { + timestampProvider = new DefaultTimestampProvider(); + } + properties.put(GormProperties.LAST_UPDATED, timestampProvider.createTimestamp(lastUpdated.getType())); + } + + JpaQueryInfo jpaQueryInfo = builder.buildUpdate(properties); + + org.hibernate.query.Query query = session.createQuery(jpaQueryInfo.getQuery()); + getHibernateTemplate().applySettings(query); + List parameters = jpaQueryInfo.getParameters(); + if (parameters != null) { + for (int i = 0, count = parameters.size(); i < count; i++) { + query.setParameter(JpaQueryBuilder.PARAMETER_NAME_PREFIX + (i + 1), parameters.get(i)); + } + } + + HibernateHqlQuery hqlQuery = new HibernateHqlQuery(HibernateSession.this, targetEntity, query); + ApplicationEventPublisher applicationEventPublisher = datastore.getApplicationEventPublisher(); + applicationEventPublisher.publishEvent(new PreQueryEvent(datastore, hqlQuery)); + int result = query.executeUpdate(); + applicationEventPublisher.publishEvent(new PostQueryEvent(datastore, hqlQuery, Collections.singletonList(result))); + return result; + }); + } + + public List retrieveAll(final Class type, final Iterable keys) { + final PersistentEntity persistentEntity = getMappingContext().getPersistentEntity(type.getName()); + return getHibernateTemplate().execute(session -> { + final CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(type); + final Root root = criteriaQuery.from(type); + final String id = persistentEntity.getIdentity().getName(); + criteriaQuery = criteriaQuery.where( + criteriaBuilder.in( + root.get(id).in(getIterableAsCollection(keys)) + ) + ); + final org.hibernate.query.Query jpaQuery = session.createQuery(criteriaQuery); + getHibernateTemplate().applySettings(jpaQuery); + + return new HibernateHqlQuery(this, persistentEntity, jpaQuery).list(); + }); + } + + public Query createQuery(Class type) { + return createQuery(type, null); + } + + @Override + public Query createQuery(Class type, String alias) { + final PersistentEntity persistentEntity = getMappingContext().getPersistentEntity(type.getName()); + GrailsHibernateTemplate hibernateTemplate = getHibernateTemplate(); + Session currentSession = hibernateTemplate.getSessionFactory().getCurrentSession(); + final Criteria criteria = alias != null ? currentSession.createCriteria(type, alias) : currentSession.createCriteria(type); + hibernateTemplate.applySettings(criteria); + return new HibernateQuery(criteria, this, persistentEntity); + } + + protected GrailsHibernateTemplate getHibernateTemplate() { + return (GrailsHibernateTemplate) getNativeInterface(); + } + + public void setFlushMode(FlushModeType flushMode) { + if (flushMode == FlushModeType.AUTO) { + hibernateTemplate.setFlushMode(GrailsHibernateTemplate.FLUSH_AUTO); + } + else if (flushMode == FlushModeType.COMMIT) { + hibernateTemplate.setFlushMode(GrailsHibernateTemplate.FLUSH_COMMIT); + } + } + + public FlushModeType getFlushMode() { + switch (hibernateTemplate.getFlushMode()) { + case GrailsHibernateTemplate.FLUSH_AUTO: return FlushModeType.AUTO; + case GrailsHibernateTemplate.FLUSH_COMMIT: return FlushModeType.COMMIT; + case GrailsHibernateTemplate.FLUSH_ALWAYS: return FlushModeType.AUTO; + default: return FlushModeType.AUTO; + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/IHibernateTemplate.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/IHibernateTemplate.java new file mode 100644 index 00000000000..90dcebeed9a --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/IHibernateTemplate.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import java.io.Serializable; +import java.util.Collection; + +import groovy.lang.Closure; + +import org.hibernate.Criteria; +import org.hibernate.LockMode; +import org.hibernate.SessionFactory; +import org.hibernate.query.Query; + +/** + * Template interface that can be used with both Hibernate 3 and Hibernate 4 + * + * @author Burt Beckwith + * @author Graeme Rocher + */ +public interface IHibernateTemplate { + + Serializable save(Object o); + + void refresh(Object o); + + void lock(Object o, LockMode lockMode); + + void flush(); + + void clear(); + + void evict(Object o); + + boolean contains(Object o); + + void setFlushMode(int mode); + + int getFlushMode(); + + void deleteAll(Collection list); + + void applySettings(Query query); + + void applySettings(Criteria criteria); + + T get(Class type, Serializable key); + + T get(Class type, Serializable key, LockMode mode); + + T load(Class type, Serializable key); + + void delete(Object o); + + SessionFactory getSessionFactory(); + + T execute(Closure callable); + + T executeWithNewSession(Closure callable); + + T1 executeWithExistingOrCreateNewSession(SessionFactory sessionFactory, Closure callable); +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/InstanceApiHelper.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/InstanceApiHelper.java new file mode 100644 index 00000000000..550f754d649 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/InstanceApiHelper.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import org.hibernate.FlushMode; + +import org.grails.orm.hibernate.GrailsHibernateTemplate.HibernateCallback; + +/** + * Workaround for VerifyErrors in Groovy when using a HibernateCallback. + * + * @author Burt Beckwith + */ +public class InstanceApiHelper { + + protected GrailsHibernateTemplate hibernateTemplate; + + public InstanceApiHelper(final GrailsHibernateTemplate hibernateTemplate) { + this.hibernateTemplate = hibernateTemplate; + } + + public void delete(final Object obj, final boolean flush) { + hibernateTemplate.execute((HibernateCallback) session -> { + session.delete(obj); + if (flush) { + session.flush(); + } + return null; + }); + } + + public void setFlushModeManual() { + hibernateTemplate.execute((HibernateCallback) session -> { + session.setHibernateFlushMode(FlushMode.MANUAL); + return null; + }); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/MetadataIntegrator.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/MetadataIntegrator.groovy new file mode 100644 index 00000000000..bc2199e98c1 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/MetadataIntegrator.groovy @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate + +import groovy.transform.CompileStatic + +import org.hibernate.boot.Metadata +import org.hibernate.engine.spi.SessionFactoryImplementor +import org.hibernate.integrator.spi.Integrator +import org.hibernate.service.spi.SessionFactoryServiceRegistry + +@CompileStatic +class MetadataIntegrator implements Integrator { + + Metadata metadata + + @Override + void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { + this.metadata = metadata + } + + @Override + void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { + // noop + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SessionFactoryHolder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SessionFactoryHolder.java new file mode 100644 index 00000000000..eac34d92754 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SessionFactoryHolder.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import org.hibernate.SessionFactory; + +/** + * Holds a reference to the SessionFactory, used to allow proxying of the + * session factory in development mode. + * + * @since 2.0 + * @author Graeme Rocher + */ +public class SessionFactoryHolder { + + public static final String BEAN_ID = "org.grails.internal.SESSION_FACTORY_HOLDER"; + + private SessionFactory sessionFactory; + + public SessionFactory getSessionFactory() { + return sessionFactory; + } + + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/access/TraitPropertyAccessStrategy.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/access/TraitPropertyAccessStrategy.java new file mode 100644 index 00000000000..65fc924d928 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/access/TraitPropertyAccessStrategy.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.access; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.codehaus.groovy.transform.trait.Traits; + +import org.hibernate.property.access.spi.Getter; +import org.hibernate.property.access.spi.GetterFieldImpl; +import org.hibernate.property.access.spi.GetterMethodImpl; +import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.property.access.spi.PropertyAccessStrategy; +import org.hibernate.property.access.spi.Setter; +import org.hibernate.property.access.spi.SetterFieldImpl; +import org.hibernate.property.access.spi.SetterMethodImpl; + +import org.springframework.util.ReflectionUtils; + +import org.grails.datastore.mapping.reflect.NameUtils; + +/** + * Support reading and writing trait fields with Hibernate 5+ + * + * @author Graeme Rocher + * @since 6.1.3 + */ +public class TraitPropertyAccessStrategy implements PropertyAccessStrategy { + @Override + public PropertyAccess buildPropertyAccess(Class containerJavaType, String propertyName) { + Method readMethod = ReflectionUtils.findMethod(containerJavaType, NameUtils.getGetterName(propertyName)); + if (readMethod == null) { + // See https://issues.apache.org/jira/browse/GROOVY-11512 + readMethod = ReflectionUtils.findMethod(containerJavaType, NameUtils.getGetterName(propertyName, true)); + if (readMethod != null && readMethod.getReturnType() != Boolean.class && readMethod.getReturnType() != boolean.class) { + readMethod = null; + } + } + + if (readMethod == null) { + throw new IllegalStateException("TraitPropertyAccessStrategy used on property [" + propertyName + "] of class [" + containerJavaType.getName() + "] that is not provided by a trait!"); + } + else { + + Traits.Implemented traitImplemented = readMethod.getAnnotation(Traits.Implemented.class); + final String traitFieldName; + if (traitImplemented == null) { + Traits.TraitBridge traitBridge = readMethod.getAnnotation(Traits.TraitBridge.class); + if (traitBridge != null) { + traitFieldName = getTraitFieldName(traitBridge.traitClass(), propertyName); + } + else { + throw new IllegalStateException("TraitPropertyAccessStrategy used on property [" + propertyName + "] of class [" + containerJavaType.getName() + "] that is not provided by a trait!"); + } + } + else { + traitFieldName = getTraitFieldName(readMethod.getDeclaringClass(), propertyName); + } + + Field field = ReflectionUtils.findField(containerJavaType, traitFieldName); + final Getter getter; + final Setter setter; + if (field == null) { + getter = new GetterMethodImpl(containerJavaType, propertyName, readMethod); + Method writeMethod = ReflectionUtils.findMethod(containerJavaType, NameUtils.getSetterName(propertyName), readMethod.getReturnType()); + setter = new SetterMethodImpl(containerJavaType, propertyName, writeMethod); + } + else { + + getter = new GetterFieldImpl(containerJavaType, propertyName, field); + setter = new SetterFieldImpl(containerJavaType, propertyName, field); + } + + return new PropertyAccess() { + @Override + public PropertyAccessStrategy getPropertyAccessStrategy() { + return TraitPropertyAccessStrategy.this; + } + + @Override + public Getter getGetter() { + return getter; + } + + @Override + public Setter getSetter() { + return setter; + } + }; + } + } + + private String getTraitFieldName(Class traitClass, String fieldName) { + return traitClass.getName().replace('.', '_') + "__" + fieldName; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/AbstractGrailsDomainBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/AbstractGrailsDomainBinder.java new file mode 100644 index 00000000000..0ee60f90c9c --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/AbstractGrailsDomainBinder.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.grails.datastore.mapping.model.PersistentEntity; + +/** + * Handles the binding Grails domain classes and properties to the Hibernate runtime meta model. + * Based on the HbmBinder code in Hibernate core and influenced by AnnotationsBinder. + * + * @author Graeme Rocher + * @since 0.1 + */ +public abstract class AbstractGrailsDomainBinder { + protected static final Map, Mapping> MAPPING_CACHE = new HashMap<>(); + + /** + * Obtains a mapping object for the given domain class nam + * + * @param theClass The domain class in question + * @return A Mapping object or null + */ + public static Mapping getMapping(Class theClass) { + return theClass == null ? null : MAPPING_CACHE.get(theClass); + } + + /** + * Obtains a mapping object for the given domain class nam + * + * @param theClass The domain class in question + * @return A Mapping object or null + */ + static void cacheMapping(Class theClass, Mapping mapping) { + MAPPING_CACHE.put(theClass, mapping); + } + + /** + * Obtains a mapping object for the given domain class nam + * + * @param domainClass The domain class in question + * @return A Mapping object or null + */ + public static Mapping getMapping(PersistentEntity domainClass) { + return domainClass == null ? null : MAPPING_CACHE.get(domainClass.getJavaClass()); + } + + public static void clearMappingCache() { + MAPPING_CACHE.clear(); + } + + public static void clearMappingCache(Class theClass) { + String className = theClass.getName(); + for (Iterator, Mapping>> it = MAPPING_CACHE.entrySet().iterator(); it.hasNext();) { + Map.Entry, Mapping> entry = it.next(); + if (className.equals(entry.getKey().getName())) { + it.remove(); + } + } + } +} + diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/CacheConfig.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/CacheConfig.groovy new file mode 100644 index 00000000000..99702598884 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/CacheConfig.groovy @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.AutoClone +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +import org.springframework.beans.MutablePropertyValues +import org.springframework.validation.DataBinder + +/** + * Defines the cache configuration. + * + * @author Graeme Rocher + * @since 1.0 + */ +@AutoClone +@CompileStatic +@Builder(builderStrategy = SimpleStrategy, prefix = '') +class CacheConfig implements Cloneable { + + static final List USAGE_OPTIONS = ['read-only', 'read-write', 'nonstrict-read-write', 'transactional'] + static final List INCLUDE_OPTIONS = ['all', 'non-lazy'] + + /** + * The cache usage + */ + String usage = 'read-write' + /** + * Whether caching is enabled + */ + boolean enabled = false + /** + * What to include in caching + */ + String include = 'all' + + /** + * Configures a new CacheConfig instance + * + * @param config The configuration + * @return The new instance + */ + static CacheConfig configureNew(@DelegatesTo(CacheConfig) Closure config) { + CacheConfig cacheConfig = new CacheConfig() + return configureExisting(cacheConfig, config) + } + + /** + * Configures an existing CacheConfig instance + * + * @param config The configuration + * @return The new instance + */ + static CacheConfig configureExisting(CacheConfig cacheConfig, Map config) { + DataBinder dataBinder = new DataBinder(cacheConfig) + dataBinder.bind(new MutablePropertyValues(config)) + return cacheConfig + } + /** + * Configures an existing PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static CacheConfig configureExisting(CacheConfig cacheConfig, @DelegatesTo(CacheConfig) Closure config) { + config.setDelegate(cacheConfig) + config.setResolveStrategy(Closure.DELEGATE_ONLY) + config.call() + return cacheConfig + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy new file mode 100644 index 00000000000..9062a3b4d09 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.AutoClone +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +import org.springframework.beans.MutablePropertyValues +import org.springframework.validation.DataBinder + +/** + * Defines a column within the mapping. + * + * @author Graeme Rocher + * @since 1.0 + */ +@AutoClone +@CompileStatic +@Builder(builderStrategy = SimpleStrategy, prefix = '') +class ColumnConfig { + + /** + * The column name + */ + String name + /** + * The SQL type + */ + String sqlType + /** + * The enum type + */ + String enumType = 'default' + /** + * The index, can be either a boolean or a string for the name of the index + */ + def index + /** + * Whether the column is unique + */ + boolean unique = false + /** + * The length of the column + */ + int length = -1 + /** + * The precision of the column + */ + int precision = -1 + /** + * The scale of the column + */ + int scale = -1 + /** + * The default value + */ + String defaultValue + /** + * A comment to apply to the column + */ + String comment + /** + * A custom read string + */ + String read + /** + * A custom write sstring + */ + String write + + String toString() { + "column[name:$name, index:$index, unique:$unique, length:$length, precision:$precision, scale:$scale]" + } + + /** + * Configures a new PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static ColumnConfig configureNew(@DelegatesTo(ColumnConfig) Closure config) { + ColumnConfig property = new ColumnConfig() + return configureExisting(property, config) + } + + /** + * Configures a new PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static ColumnConfig configureNew(Map config) { + ColumnConfig property = new ColumnConfig() + return configureExisting(property, config) + } + /** + * Configures an existing PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static ColumnConfig configureExisting(ColumnConfig property, @DelegatesTo(ColumnConfig) Closure config) { + config.setDelegate(property) + config.setResolveStrategy(Closure.DELEGATE_ONLY) + config.call() + return property + } + + /** + * Configures an existing PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static ColumnConfig configureExisting(ColumnConfig column, Map config) { + DataBinder dataBinder = new DataBinder(column) + dataBinder.bind(new MutablePropertyValues(config)) + return column + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/CompositeIdentity.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/CompositeIdentity.groovy new file mode 100644 index 00000000000..11b09015a44 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/CompositeIdentity.groovy @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.AutoClone +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +import org.grails.datastore.mapping.config.Property + +/** + * Represents a composite identity, equivalent to Hibernate mapping. + * + * @author Graeme Rocher + * @since 1.0 + */ +@AutoClone +@Builder(builderStrategy = SimpleStrategy, prefix = '') +@CompileStatic +class CompositeIdentity extends Property { + + /** + * The property names that make up the custom identity + */ + String[] propertyNames + /** + * The composite id class + */ + Class compositeClass +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/DiscriminatorConfig.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/DiscriminatorConfig.groovy new file mode 100644 index 00000000000..46351684fff --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/DiscriminatorConfig.groovy @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +/** + * Configurations the discriminator + * + * @author Graeme Rocher + * @since 6.1 + */ +@CompileStatic +@Builder(builderStrategy = SimpleStrategy, prefix = '') +class DiscriminatorConfig { + + /** + * The discriminator value + */ + String value + + /** + * The column configuration + */ + ColumnConfig column + + /** + * The type + */ + Object type + + /** + * Whether it is insertable + */ + Boolean insertable + + /** + * The formula to use + */ + String formula + + /** + * Whether it is insertable + * + * @param insertable True if it is insertable + */ + void setInsert(boolean insertable) { + this.insertable = insertable + } + + /** + * Configures the column + * @param columnConfig The column config + * @return This discriminator config + */ + DiscriminatorConfig column(@DelegatesTo(ColumnConfig) Closure columnConfig) { + column = new ColumnConfig() + columnConfig.setDelegate(column) + columnConfig.setResolveStrategy(Closure.DELEGATE_ONLY) + columnConfig.call() + return this + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java new file mode 100644 index 00000000000..1c5715728c5 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java @@ -0,0 +1,3607 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg; + +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.SortedSet; +import java.util.StringTokenizer; + +import groovy.lang.Closure; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.transform.trait.Traits; + +import jakarta.persistence.Entity; + +import org.hibernate.FetchMode; +import org.hibernate.MappingException; +import org.hibernate.boot.internal.MetadataBuildingContextRootImpl; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.boot.spi.MetadataBuildingOptions; +import org.hibernate.boot.spi.MetadataContributor; +import org.hibernate.cfg.AccessType; +import org.hibernate.cfg.BinderHelper; +import org.hibernate.cfg.ImprovedNamingStrategy; +import org.hibernate.cfg.NamingStrategy; +import org.hibernate.cfg.SecondPass; +import org.hibernate.engine.OptimisticLockStyle; +import org.hibernate.engine.spi.FilterDefinition; +import org.hibernate.engine.spi.PersistentAttributeInterceptable; +import org.hibernate.id.PersistentIdentifierGenerator; +import org.hibernate.id.enhanced.SequenceStyleGenerator; +import org.hibernate.mapping.Backref; +import org.hibernate.mapping.Bag; +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.Component; +import org.hibernate.mapping.DependantValue; +import org.hibernate.mapping.Formula; +import org.hibernate.mapping.IndexBackref; +import org.hibernate.mapping.IndexedCollection; +import org.hibernate.mapping.JoinedSubclass; +import org.hibernate.mapping.KeyValue; +import org.hibernate.mapping.ManyToOne; +import org.hibernate.mapping.OneToMany; +import org.hibernate.mapping.OneToOne; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.mapping.RootClass; +import org.hibernate.mapping.Selectable; +import org.hibernate.mapping.SimpleValue; +import org.hibernate.mapping.SingleTableSubclass; +import org.hibernate.mapping.Subclass; +import org.hibernate.mapping.Table; +import org.hibernate.mapping.UnionSubclass; +import org.hibernate.mapping.UniqueKey; +import org.hibernate.mapping.Value; +import org.hibernate.persister.entity.UnionSubclassEntityPersister; +import org.hibernate.type.EnumType; +import org.hibernate.type.ForeignKeyDirection; +import org.hibernate.type.IntegerType; +import org.hibernate.type.LongType; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.TimestampType; +import org.hibernate.type.Type; +import org.hibernate.usertype.UserCollectionType; +import org.jboss.jandex.IndexView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.util.StringUtils; + +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.datastore.mapping.core.connections.ConnectionSourcesSupport; +import org.grails.datastore.mapping.model.DatastoreConfigurationException; +import org.grails.datastore.mapping.model.MappingContext; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.config.GormProperties; +import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.model.types.Basic; +import org.grails.datastore.mapping.model.types.Embedded; +import org.grails.datastore.mapping.model.types.ManyToMany; +import org.grails.datastore.mapping.model.types.TenantId; +import org.grails.datastore.mapping.model.types.ToMany; +import org.grails.datastore.mapping.model.types.ToOne; +import org.grails.datastore.mapping.reflect.EntityReflector; +import org.grails.datastore.mapping.reflect.NameUtils; +import org.grails.orm.hibernate.access.TraitPropertyAccessStrategy; + +/** + * Handles the binding Grails domain classes and properties to the Hibernate runtime meta model. + * Based on the HbmBinder code in Hibernate core and influenced by AnnotationsBinder. + * + * @author Graeme Rocher + * @since 0.1 + */ +@SuppressWarnings("WeakerAccess") +public class GrailsDomainBinder implements MetadataContributor { + + protected static final String CASCADE_ALL_DELETE_ORPHAN = "all-delete-orphan"; + protected static final String FOREIGN_KEY_SUFFIX = "_id"; + protected static final String STRING_TYPE = "string"; + protected static final String EMPTY_PATH = ""; + protected static final char UNDERSCORE = '_'; + protected static final String CASCADE_ALL = "all"; + protected static final String CASCADE_SAVE_UPDATE = "save-update"; + protected static final String CASCADE_NONE = "none"; + protected static final String BACKTICK = "`"; + + protected static final String ENUM_TYPE_CLASS = "org.hibernate.type.EnumType"; + protected static final String ENUM_CLASS_PROP = "enumClass"; + protected static final String ENUM_TYPE_PROP = "type"; + protected static final String DEFAULT_ENUM_TYPE = "default"; + protected static final Logger LOG = LoggerFactory.getLogger(GrailsDomainBinder.class); + public static final String SEQUENCE_KEY = "sequence"; + /** + * Overrideable naming strategy. Defaults to ImprovedNamingStrategy but can + * be configured in DataSource.groovy via hibernate.naming_strategy = .... + */ + public static Map NAMING_STRATEGIES = new HashMap<>(); + + static { + NAMING_STRATEGIES.put(ConnectionSource.DEFAULT, ImprovedNamingStrategy.INSTANCE); + } + + protected final CollectionType CT = new CollectionType(null, this) { + public Collection create(ToMany property, PersistentClass owner, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + return null; + } + }; + + protected final String sessionFactoryName; + protected final String dataSourceName; + protected final HibernateMappingContext hibernateMappingContext; + protected Closure defaultMapping; + protected PersistentEntityNamingStrategy namingStrategy; + protected MetadataBuildingContext metadataBuildingContext; + + public GrailsDomainBinder( + String dataSourceName, + String sessionFactoryName, + HibernateMappingContext hibernateMappingContext) { + this.sessionFactoryName = sessionFactoryName; + this.dataSourceName = dataSourceName; + this.hibernateMappingContext = hibernateMappingContext; + // pre-build mappings + for (PersistentEntity persistentEntity : hibernateMappingContext.getPersistentEntities()) { + evaluateMapping(persistentEntity); + } + } + + /** + * The default mapping defined by {@link org.grails.datastore.mapping.config.Settings#SETTING_DEFAULT_MAPPING} + * @param defaultMapping The default mapping + */ + public void setDefaultMapping(Closure defaultMapping) { + this.defaultMapping = defaultMapping; + } + + /** + * + * @param namingStrategy Custom naming strategy to plugin into table naming + */ + public void setNamingStrategy(PersistentEntityNamingStrategy namingStrategy) { + this.namingStrategy = namingStrategy; + } + + @Override + public void contribute(InFlightMetadataCollector metadataCollector, IndexView jandexIndex) { + MetadataBuildingOptions options = metadataCollector.getMetadataBuildingOptions(); + ClassLoaderService classLoaderService = options.getServiceRegistry().getService(ClassLoaderService.class); + + this.metadataBuildingContext = new MetadataBuildingContextRootImpl( + metadataCollector.getBootstrapContext(), + options, + metadataCollector + ); + + java.util.Collection persistentEntities = hibernateMappingContext.getPersistentEntities(); + for (PersistentEntity persistentEntity : persistentEntities) { + if (!persistentEntity.getJavaClass().isAnnotationPresent(Entity.class)) { + if (ConnectionSourcesSupport.usesConnectionSource(persistentEntity, dataSourceName) && persistentEntity.isRoot()) { + bindRoot((HibernatePersistentEntity) persistentEntity, metadataCollector, sessionFactoryName); + } + } + } + } + + /** + * Override the default naming strategy for the default datasource given a Class or a full class name. + * @param strategy the class or name + * @throws ClassNotFoundException When the class was not found for specified strategy + * @throws InstantiationException When an error occurred instantiating the strategy + * @throws IllegalAccessException When an error occurred instantiating the strategy + */ + public static void configureNamingStrategy(final Object strategy) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + configureNamingStrategy(ConnectionSource.DEFAULT, strategy); + } + + /** + * Override the default naming strategy given a Class or a full class name, + * or an instance of a NamingStrategy. + * + * @param datasourceName the datasource name + * @param strategy the class, name, or instance + * @throws ClassNotFoundException When the class was not found for specified strategy + * @throws InstantiationException When an error occurred instantiating the strategy + * @throws IllegalAccessException When an error occurred instantiating the strategy + */ + public static void configureNamingStrategy(final String datasourceName, final Object strategy) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + Class namingStrategyClass = null; + NamingStrategy namingStrategy; + if (strategy instanceof Class) { + namingStrategyClass = (Class) strategy; + } + else if (strategy instanceof CharSequence) { + namingStrategyClass = Thread.currentThread().getContextClassLoader().loadClass(strategy.toString()); + } + + if (namingStrategyClass == null) { + namingStrategy = (NamingStrategy) strategy; + } + else { + namingStrategy = (NamingStrategy) namingStrategyClass.newInstance(); + } + + NAMING_STRATEGIES.put(datasourceName, namingStrategy); + } + + protected void bindMapSecondPass(ToMany property, InFlightMetadataCollector mappings, + Map persistentClasses, org.hibernate.mapping.Map map, String sessionFactoryBeanName) { + bindCollectionSecondPass(property, mappings, persistentClasses, map, sessionFactoryBeanName); + + SimpleValue value = new SimpleValue(metadataBuildingContext, map.getCollectionTable()); + + bindSimpleValue(getIndexColumnType(property, STRING_TYPE), value, true, + getIndexColumnName(property, sessionFactoryBeanName), mappings); + PropertyConfig pc = getPropertyConfig(property); + if (pc != null && pc.getIndexColumn() != null) { + bindColumnConfigToColumn(property, getColumnForSimpleValue(value), getSingleColumnConfig(pc.getIndexColumn())); + } + + if (!value.isTypeSpecified()) { + throw new MappingException("map index element must specify a type: " + map.getRole()); + } + map.setIndex(value); + + if (!(property instanceof org.grails.datastore.mapping.model.types.OneToMany) && !(property instanceof ManyToMany)) { + + SimpleValue elt = new SimpleValue(metadataBuildingContext, map.getCollectionTable()); + map.setElement(elt); + + String typeName = getTypeName(property, getPropertyConfig(property), getMapping(property.getOwner())); + if (typeName == null) { + + if (property instanceof Basic) { + Basic basic = (Basic) property; + typeName = basic.getComponentType().getName(); + } + } + if (typeName == null || typeName.equals(Object.class.getName())) { + typeName = StandardBasicTypes.STRING.getName(); + } + bindSimpleValue(typeName, elt, false, getMapElementName(property, sessionFactoryBeanName), mappings); + + elt.setTypeName(typeName); + } + + map.setInverse(false); + } + + protected ColumnConfig getSingleColumnConfig(PropertyConfig propertyConfig) { + if (propertyConfig != null) { + List columns = propertyConfig.getColumns(); + if (columns != null && !columns.isEmpty()) { + return columns.get(0); + } + } + return null; + } + + protected void bindListSecondPass(ToMany property, InFlightMetadataCollector mappings, + Map persistentClasses, org.hibernate.mapping.List list, String sessionFactoryBeanName) { + + bindCollectionSecondPass(property, mappings, persistentClasses, list, sessionFactoryBeanName); + + String columnName = getIndexColumnName(property, sessionFactoryBeanName); + final boolean isManyToMany = property instanceof ManyToMany; + + if (isManyToMany && !property.isOwningSide()) { + throw new MappingException("Invalid association [" + property + + "]. List collection types only supported on the owning side of a many-to-many relationship."); + } + + Table collectionTable = list.getCollectionTable(); + SimpleValue iv = new SimpleValue(metadataBuildingContext, collectionTable); + bindSimpleValue("integer", iv, true, columnName, mappings); + iv.setTypeName("integer"); + list.setIndex(iv); + list.setBaseIndex(0); + list.setInverse(false); + + Value v = list.getElement(); + v.createForeignKey(); + + if (property.isBidirectional()) { + + String entityName; + Value element = list.getElement(); + if (element instanceof ManyToOne) { + ManyToOne manyToOne = (ManyToOne) element; + entityName = manyToOne.getReferencedEntityName(); + } else { + entityName = ((OneToMany) element).getReferencedEntityName(); + } + + PersistentClass referenced = mappings.getEntityBinding(entityName); + + Class mappedClass = referenced.getMappedClass(); + Mapping m = getMapping(mappedClass); + + boolean compositeIdProperty = isCompositeIdProperty(m, property.getInverseSide()); + if (!compositeIdProperty) { + Backref prop = new Backref(); + final PersistentEntity owner = property.getOwner(); + prop.setEntityName(owner.getName()); + prop.setName(UNDERSCORE + addUnderscore(owner.getJavaClass().getSimpleName(), property.getName()) + "Backref"); + prop.setSelectable(false); + prop.setUpdateable(false); + if (isManyToMany) { + prop.setInsertable(false); + } + prop.setCollectionRole(list.getRole()); + prop.setValue(list.getKey()); + + DependantValue value = (DependantValue) prop.getValue(); + if (!property.isCircular()) { + value.setNullable(false); + } + value.setUpdateable(true); + prop.setOptional(false); + + referenced.addProperty(prop); + } + + if ((!list.getKey().isNullable() && !list.isInverse()) || compositeIdProperty) { + IndexBackref ib = new IndexBackref(); + ib.setName(UNDERSCORE + property.getName() + "IndexBackref"); + ib.setUpdateable(false); + ib.setSelectable(false); + if (isManyToMany) { + ib.setInsertable(false); + } + ib.setCollectionRole(list.getRole()); + ib.setEntityName(list.getOwner().getEntityName()); + ib.setValue(list.getIndex()); + referenced.addProperty(ib); + } + } + } + + protected void bindCollectionSecondPass(ToMany property, InFlightMetadataCollector mappings, + Map persistentClasses, Collection collection, String sessionFactoryBeanName) { + + PersistentClass associatedClass = null; + + if (LOG.isDebugEnabled()) + LOG.debug("Mapping collection: " + + collection.getRole() + + " -> " + + collection.getCollectionTable().getName()); + + PropertyConfig propConfig = getPropertyConfig(property); + + PersistentEntity referenced = property.getAssociatedEntity(); + if (propConfig != null && StringUtils.hasText(propConfig.getSort())) { + if (!property.isBidirectional() && (property instanceof org.grails.datastore.mapping.model.types.OneToMany)) { + throw new DatastoreConfigurationException("Default sort for associations [" + property.getOwner().getName() + "->" + property.getName() + + "] are not supported with unidirectional one to many relationships."); + } + if (referenced != null) { + PersistentProperty propertyToSortBy = referenced.getPropertyByName(propConfig.getSort()); + + String associatedClassName = property.getAssociatedEntity().getName(); + + associatedClass = (PersistentClass) persistentClasses.get(associatedClassName); + if (associatedClass != null) { + collection.setOrderBy(buildOrderByClause(propertyToSortBy.getName(), associatedClass, collection.getRole(), + propConfig.getOrder() != null ? propConfig.getOrder() : "asc")); + } + } + } + + // Configure one-to-many + if (collection.isOneToMany()) { + + Mapping m = getRootMapping(referenced); + boolean tablePerSubclass = m != null && !m.getTablePerHierarchy(); + + if (referenced != null && !referenced.isRoot() && !tablePerSubclass) { + Mapping rootMapping = getRootMapping(referenced); + String discriminatorColumnName = RootClass.DEFAULT_DISCRIMINATOR_COLUMN_NAME; + + if (rootMapping != null) { + DiscriminatorConfig discriminatorConfig = rootMapping.getDiscriminator(); + if (discriminatorConfig != null) { + final ColumnConfig discriminatorColumn = discriminatorConfig.getColumn(); + if (discriminatorColumn != null) { + discriminatorColumnName = discriminatorColumn.getName(); + } + if (discriminatorConfig.getFormula() != null) { + discriminatorColumnName = discriminatorConfig.getFormula(); + } + } + } + //NOTE: this will build the set for the in clause if it has sublcasses + Set discSet = buildDiscriminatorSet((HibernatePersistentEntity) referenced); + String inclause = String.join(",", discSet); + + collection.setWhere(discriminatorColumnName + " in (" + inclause + ")"); + } + + OneToMany oneToMany = (OneToMany) collection.getElement(); + String associatedClassName = oneToMany.getReferencedEntityName(); + + associatedClass = (PersistentClass) persistentClasses.get(associatedClassName); + // if there is no persistent class for the association throw exception + if (associatedClass == null) { + throw new MappingException("Association references unmapped class: " + oneToMany.getReferencedEntityName()); + } + + oneToMany.setAssociatedClass(associatedClass); + if (shouldBindCollectionWithForeignKey(property)) { + collection.setCollectionTable(associatedClass.getTable()); + } + + bindCollectionForPropertyConfig(collection, propConfig); + } + + final boolean isManyToMany = property instanceof ManyToMany; + if (referenced != null && !isManyToMany && referenced.isMultiTenant()) { + String filterCondition = getMultiTenantFilterCondition(sessionFactoryBeanName, referenced); + if (filterCondition != null) { + if (isUnidirectionalOneToMany(property)) { + collection.addManyToManyFilter(GormProperties.TENANT_IDENTITY, filterCondition, true, Collections.emptyMap(), Collections.emptyMap()); + } else { + collection.addFilter(GormProperties.TENANT_IDENTITY, filterCondition, true, Collections.emptyMap(), Collections.emptyMap()); + } + } + } + + if (isSorted(property)) { + collection.setSorted(true); + } + + // setup the primary key references + DependantValue key = createPrimaryKeyValue(mappings, property, collection, persistentClasses); + + // link a bidirectional relationship + if (property.isBidirectional()) { + Association otherSide = property.getInverseSide(); + if ((otherSide instanceof org.grails.datastore.mapping.model.types.ToOne) && shouldBindCollectionWithForeignKey(property)) { + linkBidirectionalOneToMany(collection, associatedClass, key, otherSide); + } else if ((otherSide instanceof ManyToMany) || Map.class.isAssignableFrom(property.getType())) { + bindDependentKeyValue(property, key, mappings, sessionFactoryBeanName); + } + } else { + if (hasJoinKeyMapping(propConfig)) { + bindSimpleValue("long", key, false, propConfig.getJoinTable().getKey().getName(), mappings); + } else { + bindDependentKeyValue(property, key, mappings, sessionFactoryBeanName); + } + } + collection.setKey(key); + + // get cache config + if (propConfig != null) { + CacheConfig cacheConfig = propConfig.getCache(); + if (cacheConfig != null) { + collection.setCacheConcurrencyStrategy(cacheConfig.getUsage()); + } + } + + // if we have a many-to-many + if (isManyToMany || isBidirectionalOneToManyMap(property)) { + PersistentProperty otherSide = property.getInverseSide(); + + if (property.isBidirectional()) { + if (LOG.isDebugEnabled()) + LOG.debug("[GrailsDomainBinder] Mapping other side " + otherSide.getOwner().getName() + "." + otherSide.getName() + " -> " + collection.getCollectionTable().getName() + " as ManyToOne"); + ManyToOne element = new ManyToOne(metadataBuildingContext, collection.getCollectionTable()); + bindManyToMany((Association) otherSide, element, mappings, sessionFactoryBeanName); + collection.setElement(element); + bindCollectionForPropertyConfig(collection, propConfig); + if (property.isCircular()) { + collection.setInverse(false); + } + } else { + // TODO support unidirectional many-to-many + } + } else if (shouldCollectionBindWithJoinColumn(property)) { + bindCollectionWithJoinTable(property, mappings, collection, propConfig, sessionFactoryBeanName); + + } else if (isUnidirectionalOneToMany(property)) { + // for non-inverse one-to-many, with a not-null fk, add a backref! + // there are problems with list and map mappings and join columns relating to duplicate key constraints + // TODO change this when HHH-1268 is resolved + bindUnidirectionalOneToMany((org.grails.datastore.mapping.model.types.OneToMany) property, mappings, collection); + } + } + + private String getMultiTenantFilterCondition(String sessionFactoryBeanName, PersistentEntity referenced) { + TenantId tenantId = referenced.getTenantId(); + if (tenantId != null) { + String defaultColumnName = getDefaultColumnName(tenantId, sessionFactoryBeanName); + return ":tenantId = " + defaultColumnName; + } + else { + return null; + } + } + + @SuppressWarnings("unchecked") + protected String buildOrderByClause(String hqlOrderBy, PersistentClass associatedClass, String role, String defaultOrder) { + String orderByString = null; + if (hqlOrderBy != null) { + List properties = new ArrayList<>(); + List ordering = new ArrayList<>(); + StringBuilder orderByBuffer = new StringBuilder(); + if (hqlOrderBy.length() == 0) { + //order by id + Iterator it = associatedClass.getIdentifier().getColumnIterator(); + while (it.hasNext()) { + Selectable col = (Selectable) it.next(); + orderByBuffer.append(col.getText()).append(" asc").append(", "); + } + } + else { + StringTokenizer st = new StringTokenizer(hqlOrderBy, " ,", false); + String currentOrdering = defaultOrder; + //FIXME make this code decent + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (isNonPropertyToken(token)) { + if (currentOrdering != null) { + throw new DatastoreConfigurationException( + "Error while parsing sort clause: " + hqlOrderBy + + " (" + role + ")" + ); + } + currentOrdering = token; + } + else { + //Add ordering of the previous + if (currentOrdering == null) { + //default ordering + ordering.add("asc"); + } + else { + ordering.add(currentOrdering); + currentOrdering = null; + } + properties.add(token); + } + } + ordering.remove(0); //first one is the algorithm starter + // add last one ordering + if (currentOrdering == null) { + //default ordering + ordering.add(defaultOrder); + } + else { + ordering.add(currentOrdering); + currentOrdering = null; + } + int index = 0; + + for (String property : properties) { + Property p = BinderHelper.findPropertyByName(associatedClass, property); + if (p == null) { + throw new DatastoreConfigurationException( + "property from sort clause not found: " + + associatedClass.getEntityName() + "." + property + ); + } + PersistentClass pc = p.getPersistentClass(); + String table; + if (pc == null) { + table = ""; + } + + else if (pc == associatedClass || + (associatedClass instanceof SingleTableSubclass && + pc.getMappedClass().isAssignableFrom(associatedClass.getMappedClass()))) { + table = ""; + } else { + table = pc.getTable().getQuotedName() + "."; + } + + Iterator propertyColumns = p.getColumnIterator(); + while (propertyColumns.hasNext()) { + Selectable column = (Selectable) propertyColumns.next(); + orderByBuffer.append(table) + .append(column.getText()) + .append(" ") + .append(ordering.get(index)) + .append(", "); + } + index++; + } + } + orderByString = orderByBuffer.substring(0, orderByBuffer.length() - 2); + } + return orderByString; + } + + protected boolean isNonPropertyToken(String token) { + if (" ".equals(token)) return true; + if (",".equals(token)) return true; + if (token.equalsIgnoreCase("desc")) return true; + if (token.equalsIgnoreCase("asc")) return true; + return false; + } + + protected Set buildDiscriminatorSet(HibernatePersistentEntity domainClass) { + Set theSet = new HashSet<>(); + + Mapping mapping = domainClass.getMapping().getMappedForm(); + String discriminator = domainClass.getName(); + if (mapping != null && mapping.getDiscriminator() != null) { + DiscriminatorConfig discriminatorConfig = mapping.getDiscriminator(); + if (discriminatorConfig.getValue() != null) { + discriminator = discriminatorConfig.getValue(); + } + } + Mapping rootMapping = getRootMapping(domainClass); + String quote = "'"; + if (rootMapping != null && rootMapping.getDatasources() != null) { + DiscriminatorConfig discriminatorConfig = rootMapping.getDiscriminator(); + if (discriminatorConfig != null && discriminatorConfig.getType() != null && !discriminatorConfig.getType().equals("string")) + quote = ""; + } + theSet.add(quote + discriminator + quote); + + final java.util.Collection childEntities = domainClass.getMappingContext().getDirectChildEntities(domainClass); + for (PersistentEntity subClass : childEntities) { + theSet.addAll(buildDiscriminatorSet((HibernatePersistentEntity) subClass)); + } + return theSet; + } + + protected Mapping getRootMapping(PersistentEntity referenced) { + if (referenced == null) return null; + Class current = referenced.getJavaClass(); + while (true) { + Class superClass = current.getSuperclass(); + if (Object.class.equals(superClass)) break; + current = superClass; + } + + return getMapping(current); + } + + protected boolean isBidirectionalOneToManyMap(Association property) { + return Map.class.isAssignableFrom(property.getType()) && property.isBidirectional(); + } + + protected void bindCollectionWithJoinTable(ToMany property, + InFlightMetadataCollector mappings, Collection collection, PropertyConfig config, String sessionFactoryBeanName) { + + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + + SimpleValue element; + final boolean isBasicCollectionType = property instanceof Basic; + if (isBasicCollectionType) { + element = new SimpleValue(metadataBuildingContext, collection.getCollectionTable()); + } + else { + // for a normal unidirectional one-to-many we use a join column + element = new ManyToOne(metadataBuildingContext, collection.getCollectionTable()); + bindUnidirectionalOneToManyInverseValues(property, (ManyToOne) element); + } + collection.setInverse(false); + + String columnName; + + final boolean hasJoinColumnMapping = hasJoinColumnMapping(config); + if (isBasicCollectionType) { + final Class referencedType = ((Basic) property).getComponentType(); + String className = referencedType.getName(); + final boolean isEnum = referencedType.isEnum(); + if (hasJoinColumnMapping) { + columnName = config.getJoinTable().getColumn().getName(); + } + else { + columnName = isEnum ? namingStrategy.propertyToColumnName(className) : + addUnderscore(namingStrategy.propertyToColumnName(property.getName()), + namingStrategy.propertyToColumnName(className)); + } + + if (isEnum) { + bindEnumType(property, referencedType, element, columnName); + } + else { + + String typeName = getTypeName(property, config, getMapping(property.getOwner())); + if (typeName == null) { + Type type = mappings.getTypeConfiguration().getBasicTypeRegistry().getRegisteredType(className); + if (type != null) { + typeName = type.getName(); + } + } + if (typeName == null) { + String domainName = property.getOwner().getName(); + throw new MappingException("Missing type or column for column[" + columnName + "] on domain[" + domainName + "] referencing[" + className + "]"); + } + + bindSimpleValue(typeName, element, true, columnName, mappings); + if (hasJoinColumnMapping) { + bindColumnConfigToColumn(property, getColumnForSimpleValue(element), config.getJoinTable().getColumn()); + } + } + } else { + final PersistentEntity domainClass = property.getAssociatedEntity(); + + Mapping m = getMapping(domainClass); + if (hasCompositeIdentifier(m)) { + CompositeIdentity ci = (CompositeIdentity) m.getIdentity(); + bindCompositeIdentifierToManyToOne(property, element, ci, domainClass, + EMPTY_PATH, sessionFactoryBeanName); + } + else { + if (hasJoinColumnMapping) { + columnName = config.getJoinTable().getColumn().getName(); + } + else { + columnName = namingStrategy.propertyToColumnName(NameUtils.decapitalize(domainClass.getName())) + FOREIGN_KEY_SUFFIX; + } + + bindSimpleValue("long", element, true, columnName, mappings); + } + } + + collection.setElement(element); + + bindCollectionForPropertyConfig(collection, config); + } + + protected String addUnderscore(String s1, String s2) { + return removeBackticks(s1) + UNDERSCORE + removeBackticks(s2); + } + + protected String removeBackticks(String s) { + return s.startsWith("`") && s.endsWith("`") ? s.substring(1, s.length() - 1) : s; + } + + protected Column getColumnForSimpleValue(SimpleValue element) { + return (Column) element.getColumnIterator().next(); + } + + protected String getTypeName(PersistentProperty property, PropertyConfig config, Mapping mapping) { + if (config != null && config.getType() != null) { + final Object typeObj = config.getType(); + if (typeObj instanceof Class) { + return ((Class) typeObj).getName(); + } + return typeObj.toString(); + } + + if (mapping != null) { + return mapping.getTypeName(property.getType()); + } + + return null; + } + + protected void bindColumnConfigToColumn(PersistentProperty property, Column column, ColumnConfig columnConfig) { + final PropertyConfig mappedForm = property != null ? (PropertyConfig) property.getMapping().getMappedForm() : null; + boolean allowUnique = mappedForm != null && !mappedForm.isUniqueWithinGroup(); + + if (columnConfig == null) { + return; + } + + if (columnConfig.getLength() != -1) { + column.setLength(columnConfig.getLength()); + } + if (columnConfig.getPrecision() != -1) { + column.setPrecision(columnConfig.getPrecision()); + } + if (columnConfig.getScale() != -1) { + column.setScale(columnConfig.getScale()); + } + if (columnConfig.getSqlType() != null && !columnConfig.getSqlType().isEmpty()) { + column.setSqlType(columnConfig.getSqlType()); + } + if (allowUnique) { + column.setUnique(columnConfig.getUnique()); + } + } + + protected boolean hasJoinColumnMapping(PropertyConfig config) { + return config != null && config.getJoinTable() != null && config.getJoinTable().getColumn() != null; + } + + protected boolean shouldCollectionBindWithJoinColumn(ToMany property) { + PropertyConfig config = getPropertyConfig(property); + JoinTable jt = config != null ? config.getJoinTable() : new JoinTable(); + + return (isUnidirectionalOneToMany(property) || (property instanceof Basic)) && jt != null; + } + + /** + * @param property The property to bind + * @param manyToOne The inverse side + */ + protected void bindUnidirectionalOneToManyInverseValues(ToMany property, ManyToOne manyToOne) { + PropertyConfig config = getPropertyConfig(property); + if (config == null) { + manyToOne.setLazy(true); + } else { + manyToOne.setIgnoreNotFound(config.getIgnoreNotFound()); + final FetchMode fetch = config.getFetchMode(); + if (!fetch.equals(FetchMode.JOIN) && !fetch.equals(FetchMode.EAGER)) { + manyToOne.setLazy(true); + } + + final Boolean lazy = config.getLazy(); + if (lazy != null) { + manyToOne.setLazy(lazy); + } + } + + // set referenced entity + manyToOne.setReferencedEntityName(property.getAssociatedEntity().getName()); + } + + protected void bindCollectionForPropertyConfig(Collection collection, PropertyConfig config) { + if (config == null) { + collection.setLazy(true); + collection.setExtraLazy(false); + } else { + final FetchMode fetch = config.getFetchMode(); + if (!fetch.equals(FetchMode.JOIN) && !fetch.equals(FetchMode.EAGER)) { + collection.setLazy(true); + } + final Boolean lazy = config.getLazy(); + if (lazy != null) { + collection.setExtraLazy(lazy); + } + } + } + + public PropertyConfig getPropertyConfig(PersistentProperty property) { + return (PropertyConfig) property.getMapping().getMappedForm(); + } + + /** + * Checks whether a property is a unidirectional non-circular one-to-many + * + * @param property The property to check + * @return true if it is unidirectional and a one-to-many + */ + protected boolean isUnidirectionalOneToMany(PersistentProperty property) { + return ((property instanceof org.grails.datastore.mapping.model.types.OneToMany) && !((Association) property).isBidirectional()); + } + + /** + * Binds the primary key value column + * + * @param property The property + * @param key The key + * @param mappings The mappings + * @param sessionFactoryBeanName The name of the session factory + */ + protected void bindDependentKeyValue(PersistentProperty property, DependantValue key, + InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + + if (LOG.isDebugEnabled()) { + LOG.debug("[GrailsDomainBinder] binding [" + property.getName() + "] with dependant key"); + } + + PersistentEntity refDomainClass = property.getOwner(); + final Mapping mapping = getMapping(refDomainClass.getJavaClass()); + boolean hasCompositeIdentifier = hasCompositeIdentifier(mapping); + if ((shouldCollectionBindWithJoinColumn((ToMany) property) && hasCompositeIdentifier) || + (hasCompositeIdentifier && (property instanceof ManyToMany))) { + CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity(); + bindCompositeIdentifierToManyToOne((Association) property, key, ci, refDomainClass, EMPTY_PATH, sessionFactoryBeanName); + } + else { + bindSimpleValue(property, null, key, EMPTY_PATH, mappings, sessionFactoryBeanName); + } + } + + /** + * Creates the DependentValue object that forms a primary key reference for the collection. + * + * @param mappings + * @param property The grails property + * @param collection The collection object + * @param persistentClasses + * @return The DependantValue (key) + */ + protected DependantValue createPrimaryKeyValue(InFlightMetadataCollector mappings, PersistentProperty property, + Collection collection, Map persistentClasses) { + KeyValue keyValue; + DependantValue key; + String propertyRef = collection.getReferencedPropertyName(); + // this is to support mapping by a property + if (propertyRef == null) { + keyValue = collection.getOwner().getIdentifier(); + } else { + keyValue = (KeyValue) collection.getOwner().getProperty(propertyRef).getValue(); + } + + if (LOG.isDebugEnabled()) + LOG.debug("[GrailsDomainBinder] creating dependant key value to table [" + keyValue.getTable().getName() + "]"); + + key = new DependantValue(metadataBuildingContext, collection.getCollectionTable(), keyValue); + + key.setTypeName(null); + // make nullable and non-updateable + key.setNullable(true); + key.setUpdateable(false); + return key; + } + + /** + * Binds a unidirectional one-to-many creating a psuedo back reference property in the process. + * + * @param property + * @param mappings + * @param collection + */ + protected void bindUnidirectionalOneToMany(org.grails.datastore.mapping.model.types.OneToMany property, InFlightMetadataCollector mappings, Collection collection) { + Value v = collection.getElement(); + v.createForeignKey(); + String entityName; + if (v instanceof ManyToOne) { + ManyToOne manyToOne = (ManyToOne) v; + + entityName = manyToOne.getReferencedEntityName(); + } else { + entityName = ((OneToMany) v).getReferencedEntityName(); + } + collection.setInverse(false); + PersistentClass referenced = mappings.getEntityBinding(entityName); + Backref prop = new Backref(); + PersistentEntity owner = property.getOwner(); + prop.setEntityName(owner.getName()); + prop.setName(UNDERSCORE + addUnderscore(owner.getJavaClass().getSimpleName(), property.getName()) + "Backref"); + prop.setUpdateable(false); + prop.setInsertable(true); + prop.setCollectionRole(collection.getRole()); + prop.setValue(collection.getKey()); + prop.setOptional(true); + + referenced.addProperty(prop); + } + + protected Property getProperty(PersistentClass associatedClass, String propertyName) throws MappingException { + try { + return associatedClass.getProperty(propertyName); + } + catch (MappingException e) { + //maybe it's squirreled away in a composite primary key + if (associatedClass.getKey() instanceof Component) { + return ((Component) associatedClass.getKey()).getProperty(propertyName); + } + throw e; + } + } + + /** + * Links a bidirectional one-to-many, configuring the inverse side and using a column copy to perform the link + * + * @param collection The collection one-to-many + * @param associatedClass The associated class + * @param key The key + * @param otherSide The other side of the relationship + */ + protected void linkBidirectionalOneToMany(Collection collection, PersistentClass associatedClass, DependantValue key, PersistentProperty otherSide) { + collection.setInverse(true); + + // Iterator mappedByColumns = associatedClass.getProperty(otherSide.getName()).getValue().getColumnIterator(); + Iterator mappedByColumns = getProperty(associatedClass, otherSide.getName()).getValue().getColumnIterator(); + while (mappedByColumns.hasNext()) { + Column column = (Column) mappedByColumns.next(); + linkValueUsingAColumnCopy(otherSide, column, key); + } + } + + /** + * Establish whether a collection property is sorted + * + * @param property The property + * @return true if sorted + */ + protected boolean isSorted(PersistentProperty property) { + return SortedSet.class.isAssignableFrom(property.getType()); + } + + /** + * Binds a many-to-many relationship. A many-to-many consists of + * - a key (a DependentValue) + * - an element + * + * The element is a ManyToOne from the association table to the target entity + * + * @param property The grails property + * @param element The ManyToOne element + * @param mappings The mappings + */ + protected void bindManyToMany(Association property, ManyToOne element, + InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + bindManyToOne(property, element, EMPTY_PATH, mappings, sessionFactoryBeanName); + element.setReferencedEntityName(property.getOwner().getName()); + } + + protected void linkValueUsingAColumnCopy(PersistentProperty prop, Column column, DependantValue key) { + Column mappingColumn = new Column(); + mappingColumn.setName(column.getName()); + mappingColumn.setLength(column.getLength()); + mappingColumn.setNullable(prop.isNullable()); + mappingColumn.setSqlType(column.getSqlType()); + + mappingColumn.setValue(key); + key.addColumn(mappingColumn); + key.getTable().addColumn(mappingColumn); + } + + /** + * First pass to bind collection to Hibernate metamodel, sets up second pass + * + * @param property The GrailsDomainClassProperty instance + * @param collection The collection + * @param owner The owning persistent class + * @param mappings The Hibernate mappings instance + * @param path + */ + protected void bindCollection(ToMany property, Collection collection, + PersistentClass owner, InFlightMetadataCollector mappings, String path, String sessionFactoryBeanName) { + + // set role + String propertyName = getNameForPropertyAndPath(property, path); + collection.setRole(qualify(property.getOwner().getName(), propertyName)); + + PropertyConfig pc = getPropertyConfig(property); + // configure eager fetching + final FetchMode fetchMode = pc.getFetchMode(); + if (fetchMode == FetchMode.JOIN) { + collection.setFetchMode(FetchMode.JOIN); + } + else if (pc.getFetchMode() != null) { + collection.setFetchMode(pc.getFetchMode()); + } + else { + collection.setFetchMode(FetchMode.DEFAULT); + } + + if (pc.getCascade() != null) { + collection.setOrphanDelete(pc.getCascade().equals(CASCADE_ALL_DELETE_ORPHAN)); + } + // if it's a one-to-many mapping + if (shouldBindCollectionWithForeignKey(property)) { + OneToMany oneToMany = new OneToMany(metadataBuildingContext, collection.getOwner()); + collection.setElement(oneToMany); + bindOneToMany((org.grails.datastore.mapping.model.types.OneToMany) property, oneToMany, mappings); + } else { + bindCollectionTable(property, mappings, collection, owner.getTable(), sessionFactoryBeanName); + + if (!property.isOwningSide()) { + collection.setInverse(true); + } + } + + if (pc.getBatchSize() != null) { + collection.setBatchSize(pc.getBatchSize()); + } + + // set up second pass + if (collection instanceof org.hibernate.mapping.Set) { + mappings.addSecondPass(new GrailsCollectionSecondPass(property, mappings, collection, sessionFactoryBeanName)); + } + else if (collection instanceof org.hibernate.mapping.List) { + mappings.addSecondPass(new ListSecondPass(property, mappings, collection, sessionFactoryBeanName)); + } + else if (collection instanceof org.hibernate.mapping.Map) { + mappings.addSecondPass(new MapSecondPass(property, mappings, collection, sessionFactoryBeanName)); + } + else { // Collection -> Bag + mappings.addSecondPass(new GrailsCollectionSecondPass(property, mappings, collection, sessionFactoryBeanName)); + } + } + + /* + * We bind collections with foreign keys if specified in the mapping and only if + * it is a unidirectional one-to-many that is. + */ + protected boolean shouldBindCollectionWithForeignKey(ToMany property) { + return ((property instanceof org.grails.datastore.mapping.model.types.OneToMany) && property.isBidirectional() || + !shouldCollectionBindWithJoinColumn(property)) && + !Map.class.isAssignableFrom(property.getType()) && + !(property instanceof ManyToMany) && + !(property instanceof Basic); + } + + protected String getNameForPropertyAndPath(PersistentProperty property, String path) { + if (isNotEmpty(path)) { + return qualify(path, property.getName()); + } + return property.getName(); + } + + protected void bindCollectionTable(ToMany property, InFlightMetadataCollector mappings, + Collection collection, Table ownerTable, String sessionFactoryBeanName) { + + String owningTableSchema = ownerTable.getSchema(); + PropertyConfig config = getPropertyConfig(property); + JoinTable jt = config != null ? config.getJoinTable() : null; + + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + String tableName = (jt != null && jt.getName() != null ? jt.getName() : namingStrategy.tableName(calculateTableForMany(property, sessionFactoryBeanName))); + String schemaName = getSchemaName(mappings); + String catalogName = getCatalogName(mappings); + if (jt != null) { + if (jt.getSchema() != null) { + schemaName = jt.getSchema(); + } + if (jt.getCatalog() != null) { + catalogName = jt.getCatalog(); + } + } + + if (schemaName == null && owningTableSchema != null) { + schemaName = owningTableSchema; + } + + collection.setCollectionTable(mappings.addTable( + schemaName, catalogName, + tableName, null, false)); + } + + /** + * Calculates the mapping table for a many-to-many. One side of + * the relationship has to "own" the relationship so that there is not a situation + * where you have two mapping tables for left_right and right_left + */ + protected String calculateTableForMany(ToMany property, String sessionFactoryBeanName) { + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + + String propertyColumnName = namingStrategy.propertyToColumnName(property.getName()); + //fix for GRAILS-5895 + PropertyConfig config = getPropertyConfig(property); + JoinTable jt = config != null ? config.getJoinTable() : null; + boolean hasJoinTableMapping = jt != null && jt.getName() != null; + String left = getTableName(property.getOwner(), sessionFactoryBeanName); + + if (Map.class.isAssignableFrom(property.getType())) { + if (hasJoinTableMapping) { + return jt.getName(); + } + return addUnderscore(left, propertyColumnName); + } + + if (property instanceof Basic) { + if (hasJoinTableMapping) { + return jt.getName(); + } + return addUnderscore(left, propertyColumnName); + } + + if (property.getAssociatedEntity() == null) { + throw new MappingException("Expected an entity to be associated with the association (" + property + ") and none was found. "); + } + + String right = getTableName(property.getAssociatedEntity(), sessionFactoryBeanName); + + if (property instanceof ManyToMany) { + if (hasJoinTableMapping) { + return jt.getName(); + } + if (property.isOwningSide()) { + return addUnderscore(left, propertyColumnName); + } + return addUnderscore(right, namingStrategy.propertyToColumnName(((ManyToMany) property).getInversePropertyName())); + } + + if (shouldCollectionBindWithJoinColumn(property)) { + if (hasJoinTableMapping) { + return jt.getName(); + } + left = trimBackTigs(left); + right = trimBackTigs(right); + return addUnderscore(left, right); + } + + if (property.isOwningSide()) { + return addUnderscore(left, right); + } + return addUnderscore(right, left); + } + + protected String trimBackTigs(String tableName) { + if (tableName.startsWith(BACKTICK)) { + return tableName.substring(1, tableName.length() - 1); + } + return tableName; + } + + /** + * Evaluates the table name for the given property + * + * @param domainClass The domain class to evaluate + * @return The table name + */ + protected String getTableName(PersistentEntity domainClass, String sessionFactoryBeanName) { + Mapping m = getMapping(domainClass); + String tableName = null; + if (m != null && m.getTableName() != null) { + tableName = m.getTableName(); + } + if (tableName == null) { + String shortName = domainClass.getJavaClass().getSimpleName(); + PersistentEntityNamingStrategy namingStrategy = this.namingStrategy; + + if (namingStrategy != null) { + tableName = namingStrategy.resolveTableName(domainClass); + } + if (tableName == null) { + tableName = getNamingStrategy(sessionFactoryBeanName).classToTableName(shortName); + } + } + return tableName; + } + + protected NamingStrategy getNamingStrategy(String sessionFactoryBeanName) { + String key = "sessionFactory".equals(sessionFactoryBeanName) ? + ConnectionSource.DEFAULT : + sessionFactoryBeanName.substring("sessionFactory_".length()); + NamingStrategy namingStrategy = NAMING_STRATEGIES.get(key); + return namingStrategy != null ? namingStrategy : new ImprovedNamingStrategy(); + } + + /** + * Binds a Grails domain class to the Hibernate runtime meta model + * + * @param entity The domain class to bind + * @param mappings The existing mappings + * @param sessionFactoryBeanName the session factory bean name + * @throws MappingException Thrown if the domain class uses inheritance which is not supported + */ + public void bindClass(PersistentEntity entity, InFlightMetadataCollector mappings, String sessionFactoryBeanName) + throws MappingException { + //if (domainClass.getClazz().getSuperclass() == Object.class) { + if (entity.isRoot()) { + bindRoot((HibernatePersistentEntity) entity, mappings, sessionFactoryBeanName); + } + } + + /** + * Evaluates a Mapping object from the domain class if it has a mapping closure + * + * @param domainClass The domain class + * @return the mapping + */ + public Mapping evaluateMapping(PersistentEntity domainClass) { + return evaluateMapping(domainClass, null); + } + + public Mapping evaluateMapping(PersistentEntity domainClass, Closure defaultMapping) { + return evaluateMapping(domainClass, defaultMapping, true); + } + + public Mapping evaluateMapping(PersistentEntity domainClass, Closure defaultMapping, boolean cache) { + try { + final Mapping m = (Mapping) domainClass.getMapping().getMappedForm(); + trackCustomCascadingSaves(m, domainClass.getPersistentProperties()); + if (cache) { + AbstractGrailsDomainBinder.cacheMapping(domainClass.getJavaClass(), m); + } + return m; + } catch (Exception e) { + throw new DatastoreConfigurationException("Error evaluating ORM mappings block for domain [" + + domainClass.getName() + "]: " + e.getMessage(), e); + } + } + + /** + * Checks for any custom cascading saves set up via the mapping DSL and records them within the persistent property. + * @param mapping The Mapping. + * @param persistentProperties The persistent properties of the domain class. + */ + protected void trackCustomCascadingSaves(Mapping mapping, Iterable persistentProperties) { + for (PersistentProperty property : persistentProperties) { + PropertyConfig propConf = mapping.getPropertyConfig(property.getName()); + + if (propConf != null && propConf.getCascade() != null) { + propConf.setExplicitSaveUpdateCascade(isSaveUpdateCascade(propConf.getCascade())); + } + } + } + + /** + * Check if a save-update cascade is defined within the Hibernate cascade properties string. + * @param cascade The string containing the cascade properties. + * @return True if save-update or any other cascade property that encompasses those is present. + */ + protected boolean isSaveUpdateCascade(String cascade) { + String[] cascades = cascade.split(","); + + for (String cascadeProp : cascades) { + String trimmedProp = cascadeProp.trim(); + + if (CASCADE_SAVE_UPDATE.equals(trimmedProp) || CASCADE_ALL.equals(trimmedProp) || CASCADE_ALL_DELETE_ORPHAN.equals(trimmedProp)) { + return true; + } + } + + return false; + } + + /** + * Obtains a mapping object for the given domain class nam + * + * @param theClass The domain class in question + * @return A Mapping object or null + */ + public static Mapping getMapping(Class theClass) { + return AbstractGrailsDomainBinder.getMapping(theClass); + } + + /** + * Obtains a mapping object for the given domain class nam + * + * @param domainClass The domain class in question + * @return A Mapping object or null + */ + public static Mapping getMapping(PersistentEntity domainClass) { + return domainClass == null ? null : AbstractGrailsDomainBinder.getMapping(domainClass.getJavaClass()); + } + + public static void clearMappingCache() { + AbstractGrailsDomainBinder.clearMappingCache(); + } + + public static void clearMappingCache(Class theClass) { + // no-op, here for compatibility + } + + /** + * Binds the specified persistant class to the runtime model based on the + * properties defined in the domain class + * + * @param domainClass The Grails domain class + * @param persistentClass The persistant class + * @param mappings Existing mappings + */ + protected void bindClass(PersistentEntity domainClass, PersistentClass persistentClass, InFlightMetadataCollector mappings) { + + boolean autoImport = mappings.getMetadataBuildingOptions().getMappingDefaults().isAutoImportEnabled(); + org.grails.datastore.mapping.config.Entity mappedForm = domainClass.getMapping().getMappedForm(); + if (mappedForm instanceof Mapping) { + autoImport = ((Mapping) mappedForm).isAutoImport(); + } + + // set lazy loading for now + persistentClass.setLazy(true); + final String entityName = domainClass.getName(); + persistentClass.setEntityName(entityName); + persistentClass.setJpaEntityName(autoImport ? unqualify(entityName) : entityName); + persistentClass.setProxyInterfaceName(entityName); + persistentClass.setClassName(entityName); + + // set dynamic insert to false + persistentClass.setDynamicInsert(false); + // set dynamic update to false + persistentClass.setDynamicUpdate(false); + // set select before update to false + persistentClass.setSelectBeforeUpdate(false); + + // add import to mappings + String en = persistentClass.getEntityName(); + + if (autoImport && en.indexOf('.') > 0) { + String unqualified = unqualify(en); + mappings.addImport(unqualified, en); + } + } + + /** + * Binds a root class (one with no super classes) to the runtime meta model + * based on the supplied Grails domain class + * + * @param entity The Grails domain class + * @param mappings The Hibernate Mappings object + * @param sessionFactoryBeanName the session factory bean name + */ + public void bindRoot(HibernatePersistentEntity entity, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + if (mappings.getEntityBinding(entity.getName()) != null) { + LOG.info("[GrailsDomainBinder] Class [" + entity.getName() + "] is already mapped, skipping.. "); + return; + } + + RootClass root = new RootClass(this.metadataBuildingContext); + root.setAbstract(entity.isAbstract()); + final MappingContext mappingContext = entity.getMappingContext(); + + final java.util.Collection children = mappingContext.getDirectChildEntities(entity); + if (children.isEmpty()) { + root.setPolymorphic(false); + } + bindClass(entity, root, mappings); + + Mapping m = getMapping(entity); + + bindRootPersistentClassCommonValues(entity, root, mappings, sessionFactoryBeanName); + + if (!children.isEmpty()) { + boolean tablePerSubclass = m != null && !m.getTablePerHierarchy(); + if (!tablePerSubclass) { + // if the root class has children create a discriminator property + bindDiscriminatorProperty(root.getTable(), root, mappings); + } + // bind the sub classes + bindSubClasses(entity, root, mappings, sessionFactoryBeanName); + } + + addMultiTenantFilterIfNecessary(entity, root, mappings, sessionFactoryBeanName); + + mappings.addEntityBinding(root); + } + + /** + * Add a Hibernate filter for multitenancy if the persistent class is multitenant + * + * @param entity target persistent entity for get tenant information + * @param persistentClass persistent class for add the filter and get tenant property info + * @param mappings mappings to add the filter + * @param sessionFactoryBeanName the session factory bean name + */ + protected void addMultiTenantFilterIfNecessary( + HibernatePersistentEntity entity, PersistentClass persistentClass, + InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + if (entity.isMultiTenant()) { + TenantId tenantId = entity.getTenantId(); + + if (tenantId != null) { + String filterCondition = getMultiTenantFilterCondition(sessionFactoryBeanName, entity); + + persistentClass.addFilter( + GormProperties.TENANT_IDENTITY, + filterCondition, + true, + Collections.emptyMap(), + Collections.emptyMap() + ); + + mappings.addFilterDefinition(new FilterDefinition( + GormProperties.TENANT_IDENTITY, + filterCondition, + Collections.singletonMap(GormProperties.TENANT_IDENTITY, getProperty(persistentClass, tenantId.getName()).getType()) + )); + } + } + } + + /** + * Binds the sub classes of a root class using table-per-heirarchy inheritance mapping + * + * @param domainClass The root domain class to bind + * @param parent The parent class instance + * @param mappings The mappings instance + * @param sessionFactoryBeanName the session factory bean name + */ + protected void bindSubClasses(HibernatePersistentEntity domainClass, PersistentClass parent, + InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + final java.util.Collection subClasses = domainClass.getMappingContext().getDirectChildEntities(domainClass); + + for (PersistentEntity sub : subClasses) { + final Class javaClass = sub.getJavaClass(); + if (javaClass.getSuperclass().equals(domainClass.getJavaClass()) && ConnectionSourcesSupport.usesConnectionSource(sub, dataSourceName)) { + bindSubClass((HibernatePersistentEntity) sub, parent, mappings, sessionFactoryBeanName); + } + } + } + + /** + * Binds a sub class. + * + * @param sub The sub domain class instance + * @param parent The parent persistent class instance + * @param mappings The mappings instance + * @param sessionFactoryBeanName the session factory bean name + */ + protected void bindSubClass(HibernatePersistentEntity sub, PersistentClass parent, + InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + evaluateMapping(sub, defaultMapping); + Mapping m = getMapping(parent.getMappedClass()); + Subclass subClass; + boolean tablePerSubclass = m != null && !m.getTablePerHierarchy() && !m.isTablePerConcreteClass(); + boolean tablePerConcreteClass = m != null && m.isTablePerConcreteClass(); + final String fullName = sub.getName(); + if (tablePerSubclass) { + subClass = new JoinedSubclass(parent, this.metadataBuildingContext); + } + else if (tablePerConcreteClass) { + subClass = new UnionSubclass(parent, this.metadataBuildingContext); + } + else { + subClass = new SingleTableSubclass(parent, this.metadataBuildingContext); + // set the descriminator value as the name of the class. This is the + // value used by Hibernate to decide what the type of the class is + // to perform polymorphic queries + Mapping subMapping = getMapping(sub); + DiscriminatorConfig discriminatorConfig = subMapping != null ? subMapping.getDiscriminator() : null; + + subClass.setDiscriminatorValue(discriminatorConfig != null && discriminatorConfig.getValue() != null ? discriminatorConfig.getValue() : fullName); + + if (subMapping != null) { + configureDerivedProperties(sub, subMapping); + } + } + Integer bs = (m == null) ? null : m.getBatchSize(); + if (bs != null) { + subClass.setBatchSize(bs); + } + + if (m != null && m.getDynamicUpdate()) { + subClass.setDynamicUpdate(true); + } + if (m != null && m.getDynamicInsert()) { + subClass.setDynamicInsert(true); + } + + subClass.setCached(parent.isCached()); + + subClass.setAbstract(sub.isAbstract()); + subClass.setEntityName(fullName); + subClass.setJpaEntityName(unqualify(fullName)); + + parent.addSubclass(subClass); + mappings.addEntityBinding(subClass); + + if (tablePerSubclass) { + bindJoinedSubClass(sub, (JoinedSubclass) subClass, mappings, m, sessionFactoryBeanName); + } + else if (tablePerConcreteClass) { + bindUnionSubclass(sub, (UnionSubclass) subClass, mappings, sessionFactoryBeanName); + } + else { + bindSubClass(sub, subClass, mappings, sessionFactoryBeanName); + } + + addMultiTenantFilterIfNecessary(sub, subClass, mappings, sessionFactoryBeanName); + + final java.util.Collection childEntities = sub.getMappingContext().getDirectChildEntities(sub); + if (!childEntities.isEmpty()) { + // bind the sub classes + bindSubClasses(sub, subClass, mappings, sessionFactoryBeanName); + } + } + + public void bindUnionSubclass(HibernatePersistentEntity subClass, UnionSubclass unionSubclass, + InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { + bindClass(subClass, unionSubclass, mappings); + + Mapping subMapping = getMapping(subClass.getJavaClass()); + + if (unionSubclass.getEntityPersisterClass() == null) { + unionSubclass.getRootClass().setEntityPersisterClass( + UnionSubclassEntityPersister.class); + } + + String schema = subMapping != null && subMapping.getTable().getSchema() != null ? + subMapping.getTable().getSchema() : null; + + String catalog = subMapping != null && subMapping.getTable().getCatalog() != null ? + subMapping.getTable().getCatalog() : null; + + Table denormalizedSuperTable = unionSubclass.getSuperclass().getTable(); + Table mytable = mappings.addDenormalizedTable( + schema, + catalog, + getTableName(subClass, sessionFactoryBeanName), + unionSubclass.isAbstract() != null && unionSubclass.isAbstract(), + null, + denormalizedSuperTable + ); + unionSubclass.setTable(mytable); + unionSubclass.setClassName(subClass.getName()); + + LOG.info( + "Mapping union-subclass: " + unionSubclass.getEntityName() + + " -> " + unionSubclass.getTable().getName() + ); + + createClassProperties(subClass, unionSubclass, mappings, sessionFactoryBeanName); + + } + + /** + * Binds a joined sub-class mapping using table-per-subclass + * + * @param sub The Grails sub class + * @param joinedSubclass The Hibernate Subclass object + * @param mappings The mappings Object + * @param gormMapping The GORM mapping object + * @param sessionFactoryBeanName the session factory bean name + */ + protected void bindJoinedSubClass(HibernatePersistentEntity sub, JoinedSubclass joinedSubclass, + InFlightMetadataCollector mappings, Mapping gormMapping, String sessionFactoryBeanName) { + bindClass(sub, joinedSubclass, mappings); + + String schemaName = getSchemaName(mappings); + String catalogName = getCatalogName(mappings); + + Table mytable = mappings.addTable( + schemaName, catalogName, + getJoinedSubClassTableName(sub, joinedSubclass, null, mappings, sessionFactoryBeanName), + null, false); + + joinedSubclass.setTable(mytable); + LOG.info("Mapping joined-subclass: " + joinedSubclass.getEntityName() + + " -> " + joinedSubclass.getTable().getName()); + + SimpleValue key = new DependantValue(metadataBuildingContext, mytable, joinedSubclass.getIdentifier()); + joinedSubclass.setKey(key); + final PersistentProperty identifier = sub.getIdentity(); + String columnName = getColumnNameForPropertyAndPath(identifier, EMPTY_PATH, null, sessionFactoryBeanName); + bindSimpleValue(identifier.getType().getName(), key, false, columnName, mappings); + + joinedSubclass.createPrimaryKey(); + joinedSubclass.createForeignKey(); + + // properties + createClassProperties(sub, joinedSubclass, mappings, sessionFactoryBeanName); + } + + protected String getJoinedSubClassTableName( + HibernatePersistentEntity sub, PersistentClass model, Table denormalizedSuperTable, + InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + + String logicalTableName = unqualify(model.getEntityName()); + String physicalTableName = getTableName(sub, sessionFactoryBeanName); + + String schemaName = getSchemaName(mappings); + String catalogName = getCatalogName(mappings); + + mappings.addTableNameBinding(schemaName, catalogName, logicalTableName, physicalTableName, denormalizedSuperTable); + return physicalTableName; + } + + /** + * Binds a sub-class using table-per-hierarchy inheritance mapping + * + * @param sub The Grails domain class instance representing the sub-class + * @param subClass The Hibernate SubClass instance + * @param mappings The mappings instance + */ + protected void bindSubClass(HibernatePersistentEntity sub, Subclass subClass, InFlightMetadataCollector mappings, + String sessionFactoryBeanName) { + bindClass(sub, subClass, mappings); + + if (LOG.isDebugEnabled()) + LOG.debug("Mapping subclass: " + subClass.getEntityName() + + " -> " + subClass.getTable().getName()); + + // properties + createClassProperties(sub, subClass, mappings, sessionFactoryBeanName); + } + + /** + * Creates and binds the discriminator property used in table-per-hierarchy inheritance to + * discriminate between sub class instances + * + * @param table The table to bind onto + * @param entity The root class entity + * @param mappings The mappings instance + */ + protected void bindDiscriminatorProperty(Table table, RootClass entity, InFlightMetadataCollector mappings) { + Mapping m = getMapping(entity.getMappedClass()); + SimpleValue d = new SimpleValue(metadataBuildingContext, table); + entity.setDiscriminator(d); + DiscriminatorConfig discriminatorConfig = m != null ? m.getDiscriminator() : null; + + boolean hasDiscriminatorConfig = discriminatorConfig != null; + entity.setDiscriminatorValue(hasDiscriminatorConfig ? discriminatorConfig.getValue() : entity.getClassName()); + + if (hasDiscriminatorConfig) { + if (discriminatorConfig.getInsertable() != null) { + entity.setDiscriminatorInsertable(discriminatorConfig.getInsertable()); + } + Object type = discriminatorConfig.getType(); + if (type != null) { + if (type instanceof Class) { + d.setTypeName(((Class) type).getName()); + } + else { + d.setTypeName(type.toString()); + } + } + } + + if (hasDiscriminatorConfig && discriminatorConfig.getFormula() != null) { + Formula formula = new Formula(); + formula.setFormula(discriminatorConfig.getFormula()); + d.addFormula(formula); + } + else { + bindSimpleValue(STRING_TYPE, d, false, RootClass.DEFAULT_DISCRIMINATOR_COLUMN_NAME, mappings); + + ColumnConfig cc = !hasDiscriminatorConfig ? null : discriminatorConfig.getColumn(); + if (cc != null) { + Column c = (Column) d.getColumnIterator().next(); + if (cc.getName() != null) { + c.setName(cc.getName()); + } + bindColumnConfigToColumn(null, c, cc); + } + } + + entity.setPolymorphic(true); + } + + protected void configureDerivedProperties(PersistentEntity domainClass, Mapping m) { + for (PersistentProperty prop : domainClass.getPersistentProperties()) { + PropertyConfig propertyConfig = m.getPropertyConfig(prop.getName()); + if (propertyConfig != null && propertyConfig.getFormula() != null) { + propertyConfig.setDerived(true); + } + } + } + + /* + * Binds a persistent classes to the table representation and binds the class properties + */ + protected void bindRootPersistentClassCommonValues(HibernatePersistentEntity domainClass, + RootClass root, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + + // get the schema and catalog names from the configuration + Mapping m = getMapping(domainClass.getJavaClass()); + + String schema = getSchemaName(mappings); + String catalog = getCatalogName(mappings); + + if (m != null) { + configureDerivedProperties(domainClass, m); + CacheConfig cc = m.getCache(); + if (cc != null && cc.getEnabled()) { + root.setCacheConcurrencyStrategy(cc.getUsage()); + root.setCached(true); + if ("read-only".equals(cc.getUsage())) { + root.setMutable(false); + } + root.setLazyPropertiesCacheable(!"non-lazy".equals(cc.getInclude())); + } + + Integer bs = m.getBatchSize(); + if (bs != null) { + root.setBatchSize(bs); + } + + if (m.getDynamicUpdate()) { + root.setDynamicUpdate(true); + } + if (m.getDynamicInsert()) { + root.setDynamicInsert(true); + } + } + + final boolean hasTableDefinition = m != null && m.getTable() != null; + if (hasTableDefinition && m.getTable().getSchema() != null) { + schema = m.getTable().getSchema(); + } + if (hasTableDefinition && m.getTable().getCatalog() != null) { + catalog = m.getTable().getCatalog(); + } + + final boolean isAbstract = m != null && !m.getTablePerHierarchy() && m.isTablePerConcreteClass() && root.isAbstract(); + // create the table + Table table = mappings.addTable(schema, catalog, + getTableName(domainClass, sessionFactoryBeanName), + null, isAbstract); + root.setTable(table); + + if (LOG.isDebugEnabled()) { + LOG.debug("[GrailsDomainBinder] Mapping Grails domain class: " + domainClass.getName() + " -> " + root.getTable().getName()); + } + + bindIdentity(domainClass, root, mappings, m, sessionFactoryBeanName); + + if (m == null) { + bindVersion(domainClass.getVersion(), root, mappings, sessionFactoryBeanName); + } + else { + if (m.getVersioned()) { + bindVersion(domainClass.getVersion(), root, mappings, sessionFactoryBeanName); + } + else { + root.setOptimisticLockStyle(OptimisticLockStyle.NONE); + } + } + + root.createPrimaryKey(); + + createClassProperties(domainClass, root, mappings, sessionFactoryBeanName); + } + + protected void bindIdentity( + HibernatePersistentEntity domainClass, + RootClass root, + InFlightMetadataCollector mappings, + Mapping gormMapping, + String sessionFactoryBeanName) { + + PersistentProperty identifierProp = domainClass.getIdentity(); + if (gormMapping == null) { + if (identifierProp != null) { + bindSimpleId(identifierProp, root, mappings, null, sessionFactoryBeanName); + } + return; + } + + Object id = gormMapping.getIdentity(); + if (id instanceof CompositeIdentity) { + bindCompositeId(domainClass, root, (CompositeIdentity) id, mappings, sessionFactoryBeanName); + } else { + final Identity identity = (Identity) id; + String propertyName = identity.getName(); + if (propertyName != null) { + PersistentProperty namedIdentityProp = domainClass.getPropertyByName(propertyName); + if (namedIdentityProp == null) { + throw new MappingException("Mapping specifies an identifier property name that doesn't exist [" + propertyName + "]"); + } + if (!namedIdentityProp.equals(identifierProp)) { + identifierProp = namedIdentityProp; + } + } + bindSimpleId(identifierProp, root, mappings, identity, sessionFactoryBeanName); + } + } + + protected void bindCompositeId(PersistentEntity domainClass, RootClass root, + CompositeIdentity compositeIdentity, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + HibernatePersistentEntity hibernatePersistentEntity = (HibernatePersistentEntity) domainClass; + Component id = new Component(metadataBuildingContext, root); + id.setNullValue("undefined"); + root.setIdentifier(id); + root.setIdentifierMapper(id); + root.setEmbeddedIdentifier(true); + id.setComponentClassName(domainClass.getName()); + id.setKey(true); + id.setEmbedded(true); + + String path = qualify(root.getEntityName(), "id"); + + id.setRoleName(path); + + final PersistentProperty[] composite = hibernatePersistentEntity.getCompositeIdentity(); + for (PersistentProperty property : composite) { + if (property == null) { + throw new MappingException("Property referenced in composite-id mapping of class [" + domainClass.getName() + + "] is not a valid property!"); + } + + bindComponentProperty(id, null, property, root, "", root.getTable(), mappings, sessionFactoryBeanName); + } + } + + /** + * Creates and binds the properties for the specified Grails domain class and PersistentClass + * and binds them to the Hibernate runtime meta model + * + * @param domainClass The Grails domain class + * @param persistentClass The Hibernate PersistentClass instance + * @param mappings The Hibernate Mappings instance + * @param sessionFactoryBeanName the session factory bean name + */ + protected void createClassProperties(HibernatePersistentEntity domainClass, PersistentClass persistentClass, + InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + + final List persistentProperties = domainClass.getPersistentProperties(); + Table table = persistentClass.getTable(); + + Mapping gormMapping = domainClass.getMapping().getMappedForm(); + + if (gormMapping != null) { + table.setComment(gormMapping.getComment()); + } + + List embedded = new ArrayList<>(); + + for (PersistentProperty currentGrailsProp : persistentProperties) { + + // if its inherited skip + if (currentGrailsProp.isInherited()) { + continue; + } + if (currentGrailsProp.getName().equals(GormProperties.VERSION)) continue; + if (isCompositeIdProperty(gormMapping, currentGrailsProp)) continue; + if (isIdentityProperty(gormMapping, currentGrailsProp)) continue; + + if (LOG.isDebugEnabled()) { + LOG.debug("[GrailsDomainBinder] Binding persistent property [" + currentGrailsProp.getName() + "]"); + } + + Value value = null; + + // see if it's a collection type + CollectionType collectionType = CT.collectionTypeForClass(currentGrailsProp.getType()); + + Class userType = getUserType(currentGrailsProp); + + if (userType != null && !UserCollectionType.class.isAssignableFrom(userType)) { + if (LOG.isDebugEnabled()) { + LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as SimpleValue"); + } + value = new SimpleValue(metadataBuildingContext, table); + bindSimpleValue(currentGrailsProp, null, (SimpleValue) value, EMPTY_PATH, mappings, sessionFactoryBeanName); + } + else if (collectionType != null) { + String typeName = getTypeName(currentGrailsProp, getPropertyConfig(currentGrailsProp), gormMapping); + if ("serializable".equals(typeName)) { + value = new SimpleValue(metadataBuildingContext, table); + bindSimpleValue(typeName, (SimpleValue) value, currentGrailsProp.isNullable(), + getColumnNameForPropertyAndPath(currentGrailsProp, EMPTY_PATH, null, sessionFactoryBeanName), mappings); + } + else { + // create collection + Collection collection = collectionType.create((ToMany) currentGrailsProp, persistentClass, + EMPTY_PATH, mappings, sessionFactoryBeanName); + mappings.addCollectionBinding(collection); + value = collection; + } + } + else if (currentGrailsProp.getType().isEnum()) { + value = new SimpleValue(metadataBuildingContext, table); + bindEnumType(currentGrailsProp, (SimpleValue) value, EMPTY_PATH, sessionFactoryBeanName); + } + else if (currentGrailsProp instanceof Association) { + Association association = (Association) currentGrailsProp; + if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.ManyToOne) { + if (LOG.isDebugEnabled()) + LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as ManyToOne"); + + value = new ManyToOne(metadataBuildingContext, table); + bindManyToOne((Association) currentGrailsProp, (ManyToOne) value, EMPTY_PATH, mappings, sessionFactoryBeanName); + } + else if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.OneToOne && userType == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as OneToOne"); + } + + final boolean isHasOne = isHasOne(association); + if (isHasOne && !association.isBidirectional()) { + throw new MappingException("hasOne property [" + currentGrailsProp.getOwner().getName() + + "." + currentGrailsProp.getName() + "] is not bidirectional. Specify the other side of the relationship!"); + } + else if (canBindOneToOneWithSingleColumnAndForeignKey((Association) currentGrailsProp)) { + value = new OneToOne(metadataBuildingContext, table, persistentClass); + bindOneToOne((org.grails.datastore.mapping.model.types.OneToOne) currentGrailsProp, (OneToOne) value, EMPTY_PATH, sessionFactoryBeanName); + } + else { + if (isHasOne && association.isBidirectional()) { + value = new OneToOne(metadataBuildingContext, table, persistentClass); + bindOneToOne((org.grails.datastore.mapping.model.types.OneToOne) currentGrailsProp, (OneToOne) value, EMPTY_PATH, sessionFactoryBeanName); + } + else { + value = new ManyToOne(metadataBuildingContext, table); + bindManyToOne((Association) currentGrailsProp, (ManyToOne) value, EMPTY_PATH, mappings, sessionFactoryBeanName); + } + } + } + else if (currentGrailsProp instanceof Embedded) { + embedded.add((Embedded) currentGrailsProp); + continue; + } + } + // work out what type of relationship it is and bind value + else { + if (LOG.isDebugEnabled()) { + LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as SimpleValue"); + } + value = new SimpleValue(metadataBuildingContext, table); + bindSimpleValue(currentGrailsProp, null, (SimpleValue) value, EMPTY_PATH, mappings, sessionFactoryBeanName); + } + + if (value != null) { + Property property = createProperty(value, persistentClass, currentGrailsProp, mappings); + persistentClass.addProperty(property); + } + } + + for (Embedded association : embedded) { + Value value = new Component(metadataBuildingContext, persistentClass); + + bindComponent((Component) value, association, true, mappings, sessionFactoryBeanName); + Property property = createProperty(value, persistentClass, association, mappings); + persistentClass.addProperty(property); + } + bindNaturalIdentifier(table, gormMapping, persistentClass); + } + + private boolean isHasOne(Association association) { + return association instanceof org.grails.datastore.mapping.model.types.OneToOne && ((org.grails.datastore.mapping.model.types.OneToOne) association).isForeignKeyInChild(); + } + + protected void bindNaturalIdentifier(Table table, Mapping mapping, PersistentClass persistentClass) { + Object o = mapping != null ? mapping.getIdentity() : null; + if (!(o instanceof Identity)) { + return; + } + + Identity identity = (Identity) o; + final NaturalId naturalId = identity.getNatural(); + if (naturalId == null || naturalId.getPropertyNames().isEmpty()) { + return; + } + + UniqueKey uk = new UniqueKey(); + uk.setTable(table); + + boolean mutable = naturalId.isMutable(); + + for (String propertyName : naturalId.getPropertyNames()) { + Property property = persistentClass.getProperty(propertyName); + + property.setNaturalIdentifier(true); + if (!mutable) property.setUpdateable(false); + + uk.addColumns(property.getColumnIterator()); + } + + setGeneratedUniqueName(uk); + + table.addUniqueKey(uk); + } + + protected void setGeneratedUniqueName(UniqueKey uk) { + StringBuilder sb = new StringBuilder(uk.getTable().getName()).append('_'); + for (Object col : uk.getColumns()) { + sb.append(((Column) col).getName()).append('_'); + } + + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + md.update(sb.toString().getBytes(StandardCharsets.UTF_8)); + + String name = "UK" + new BigInteger(1, md.digest()).toString(16); + if (name.length() > 30) { + // Oracle has a 30-char limit + name = name.substring(0, 30); + } + + uk.setName(name); + } + + protected boolean canBindOneToOneWithSingleColumnAndForeignKey(Association currentGrailsProp) { + if (currentGrailsProp.isBidirectional()) { + final Association otherSide = currentGrailsProp.getInverseSide(); + if (otherSide != null) { + if (isHasOne(otherSide)) { + return false; + } + if (!currentGrailsProp.isOwningSide() && (otherSide.isOwningSide())) { + return true; + } + } + } + return false; + } + + protected boolean isIdentityProperty(Mapping gormMapping, PersistentProperty currentGrailsProp) { + if (gormMapping == null) { + return false; + } + + Object identityMapping = gormMapping.getIdentity(); + if (!(identityMapping instanceof Identity)) { + return false; + } + + String identityName = ((Identity) identityMapping).getName(); + return identityName != null && identityName.equals(currentGrailsProp.getName()); + } + + protected void bindEnumType(PersistentProperty property, SimpleValue simpleValue, + String path, String sessionFactoryBeanName) { + bindEnumType(property, property.getType(), simpleValue, + getColumnNameForPropertyAndPath(property, path, null, sessionFactoryBeanName)); + } + + protected void bindEnumType(PersistentProperty property, Class propertyType, SimpleValue simpleValue, String columnName) { + + PropertyConfig pc = getPropertyConfig(property); + final PersistentEntity owner = property.getOwner(); + String typeName = getTypeName(property, getPropertyConfig(property), getMapping(owner)); + if (typeName == null) { + Properties enumProperties = new Properties(); + enumProperties.put(ENUM_CLASS_PROP, propertyType.getName()); + + String enumType = pc == null ? DEFAULT_ENUM_TYPE : pc.getEnumType(); + boolean isDefaultEnumType = enumType.equals(DEFAULT_ENUM_TYPE); + simpleValue.setTypeName(ENUM_TYPE_CLASS); + if (isDefaultEnumType || "string".equalsIgnoreCase(enumType)) { + enumProperties.put(EnumType.TYPE, String.valueOf(Types.VARCHAR)); + enumProperties.put(EnumType.NAMED, Boolean.TRUE.toString()); + } + else if ("identity".equals(enumType)) { + simpleValue.setTypeName(IdentityEnumType.class.getName()); + } + else if (!"ordinal".equalsIgnoreCase(enumType)) { + simpleValue.setTypeName(enumType); + } + simpleValue.setTypeParameters(enumProperties); + } + else { + simpleValue.setTypeName(typeName); + } + + Table t = simpleValue.getTable(); + Column column = new Column(); + + if (owner.isRoot()) { + column.setNullable(property.isNullable()); + } else { + Mapping mapping = getMapping(owner); + if (mapping == null || mapping.getTablePerHierarchy()) { + if (LOG.isDebugEnabled()) { + LOG.debug("[GrailsDomainBinder] Sub class property [" + property.getName() + + "] for column name [" + column.getName() + "] set to nullable"); + } + column.setNullable(true); + } else { + column.setNullable(property.isNullable()); + } + } + column.setValue(simpleValue); + column.setName(columnName); + if (t != null) t.addColumn(column); + + simpleValue.addColumn(column); + + PropertyConfig propertyConfig = getPropertyConfig(property); + if (propertyConfig != null && !propertyConfig.getColumns().isEmpty()) { + bindIndex(columnName, column, propertyConfig.getColumns().get(0), t); + bindColumnConfigToColumn(property, column, propertyConfig.getColumns().get(0)); + } + } + + protected Class getUserType(PersistentProperty currentGrailsProp) { + Class userType = null; + PropertyConfig config = getPropertyConfig(currentGrailsProp); + Object typeObj = config == null ? null : config.getType(); + if (typeObj instanceof Class) { + userType = (Class) typeObj; + } else if (typeObj != null) { + String typeName = typeObj.toString(); + try { + userType = Class.forName(typeName, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + // only print a warning if the user type is in a package this excludes basic + // types like string, int etc. + if (typeName.indexOf(".") > -1) { + if (LOG.isWarnEnabled()) { + LOG.warn("UserType not found ", e); + } + } + } + } + return userType; + } + + protected boolean isCompositeIdProperty(Mapping gormMapping, PersistentProperty currentGrailsProp) { + if (gormMapping != null && gormMapping.getIdentity() != null) { + Object id = gormMapping.getIdentity(); + if (id instanceof CompositeIdentity) { + String[] propertyNames = ((CompositeIdentity) id).getPropertyNames(); + String property = currentGrailsProp.getName(); + for (String currentName : propertyNames) { + if (currentName != null && currentName.equals(property)) return true; + } + } + } + return false; + } + + protected boolean isBidirectionalManyToOne(PersistentProperty currentGrailsProp) { + return ((currentGrailsProp instanceof org.grails.datastore.mapping.model.types.ManyToOne) && ((Association) currentGrailsProp).isBidirectional()); + } + + /** + * Binds a Hibernate component type using the given GrailsDomainClassProperty instance + * + * @param component The component to bind + * @param property The property + * @param isNullable Whether it is nullable or not + * @param mappings The Hibernate Mappings object + * @param sessionFactoryBeanName the session factory bean name + */ + protected void bindComponent(Component component, Embedded property, + boolean isNullable, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + component.setEmbedded(true); + Class type = property.getType(); + String role = qualify(type.getName(), property.getName()); + component.setRoleName(role); + component.setComponentClassName(type.getName()); + + PersistentEntity domainClass = property.getAssociatedEntity(); + evaluateMapping(domainClass, defaultMapping); + final List properties = domainClass.getPersistentProperties(); + Table table = component.getOwner().getTable(); + PersistentClass persistentClass = component.getOwner(); + String path = property.getName(); + Class propertyType = property.getOwner().getJavaClass(); + + for (PersistentProperty currentGrailsProp : properties) { + if (currentGrailsProp.equals(domainClass.getIdentity())) continue; + if (currentGrailsProp.getName().equals(GormProperties.VERSION)) continue; + + if (currentGrailsProp.getType().equals(propertyType)) { + component.setParentProperty(currentGrailsProp.getName()); + continue; + } + + bindComponentProperty(component, property, currentGrailsProp, persistentClass, path, + table, mappings, sessionFactoryBeanName); + } + } + + protected void bindComponentProperty(Component component, PersistentProperty componentProperty, + PersistentProperty currentGrailsProp, PersistentClass persistentClass, + String path, Table table, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + Value value; + // see if it's a collection type + CollectionType collectionType = CT.collectionTypeForClass(currentGrailsProp.getType()); + if (collectionType != null) { + // create collection + Collection collection = collectionType.create((ToMany) currentGrailsProp, persistentClass, + path, mappings, sessionFactoryBeanName); + mappings.addCollectionBinding(collection); + value = collection; + } + // work out what type of relationship it is and bind value + else if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.ManyToOne) { + if (LOG.isDebugEnabled()) + LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as ManyToOne"); + + value = new ManyToOne(metadataBuildingContext, table); + bindManyToOne((Association) currentGrailsProp, (ManyToOne) value, path, mappings, sessionFactoryBeanName); + } else if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.OneToOne) { + if (LOG.isDebugEnabled()) + LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as OneToOne"); + + if (canBindOneToOneWithSingleColumnAndForeignKey((Association) currentGrailsProp)) { + value = new OneToOne(metadataBuildingContext, table, persistentClass); + bindOneToOne((org.grails.datastore.mapping.model.types.OneToOne) currentGrailsProp, (OneToOne) value, path, sessionFactoryBeanName); + } + else { + value = new ManyToOne(metadataBuildingContext, table); + bindManyToOne((Association) currentGrailsProp, (ManyToOne) value, path, mappings, sessionFactoryBeanName); + } + } + else if (currentGrailsProp instanceof Embedded) { + value = new Component(metadataBuildingContext, persistentClass); + bindComponent((Component) value, (Embedded) currentGrailsProp, true, mappings, sessionFactoryBeanName); + } + else { + if (LOG.isDebugEnabled()) + LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as SimpleValue"); + + value = new SimpleValue(metadataBuildingContext, table); + if (currentGrailsProp.getType().isEnum()) { + bindEnumType(currentGrailsProp, (SimpleValue) value, path, sessionFactoryBeanName); + } + else { + bindSimpleValue(currentGrailsProp, componentProperty, (SimpleValue) value, path, + mappings, sessionFactoryBeanName); + } + } + + if (value != null) { + Property persistentProperty = createProperty(value, persistentClass, currentGrailsProp, mappings); + component.addProperty(persistentProperty); + if (isComponentPropertyNullable(componentProperty)) { + final Iterator columnIterator = value.getColumnIterator(); + while (columnIterator.hasNext()) { + Column c = (Column) columnIterator.next(); + c.setNullable(true); + } + } + } + } + + protected boolean isComponentPropertyNullable(PersistentProperty componentProperty) { + if (componentProperty == null) return false; + final PersistentEntity domainClass = componentProperty.getOwner(); + final Mapping mapping = getMapping(domainClass.getJavaClass()); + return !domainClass.isRoot() && (mapping == null || mapping.isTablePerHierarchy()) || componentProperty.isNullable(); + } + + /* + * Creates a persistent class property based on the GrailDomainClassProperty instance. + */ + protected Property createProperty(Value value, PersistentClass persistentClass, PersistentProperty grailsProperty, InFlightMetadataCollector mappings) { + // set type + value.setTypeUsingReflection(persistentClass.getClassName(), grailsProperty.getName()); + + if (value.getTable() != null) { + value.createForeignKey(); + } + + Property prop = new Property(); + prop.setValue(value); + bindProperty(grailsProperty, prop, mappings); + return prop; + } + + protected void bindOneToMany(org.grails.datastore.mapping.model.types.OneToMany currentGrailsProp, OneToMany one, InFlightMetadataCollector mappings) { + one.setReferencedEntityName(currentGrailsProp.getAssociatedEntity().getName()); + one.setIgnoreNotFound(true); + } + + /** + * Binds a many-to-one relationship to the + * + */ + @SuppressWarnings("unchecked") + protected void bindManyToOne(Association property, ManyToOne manyToOne, + String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + + bindManyToOneValues(property, manyToOne); + PersistentEntity refDomainClass = property instanceof ManyToMany ? property.getOwner() : property.getAssociatedEntity(); + Mapping mapping = getMapping(refDomainClass); + boolean isComposite = hasCompositeIdentifier(mapping); + if (isComposite) { + CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity(); + bindCompositeIdentifierToManyToOne(property, manyToOne, ci, refDomainClass, path, sessionFactoryBeanName); + } + else { + if (property.isCircular() && (property instanceof ManyToMany)) { + PropertyConfig pc = getPropertyConfig(property); + + if (pc.getColumns().isEmpty()) { + mapping.getColumns().put(property.getName(), pc); + } + if (!hasJoinKeyMapping(pc)) { + JoinTable jt = new JoinTable(); + final ColumnConfig columnConfig = new ColumnConfig(); + columnConfig.setName(namingStrategy.propertyToColumnName(property.getName()) + + UNDERSCORE + FOREIGN_KEY_SUFFIX); + jt.setKey(columnConfig); + pc.setJoinTable(jt); + } + bindSimpleValue(property, manyToOne, path, pc, sessionFactoryBeanName); + } + else { + // bind column + bindSimpleValue(property, null, manyToOne, path, mappings, sessionFactoryBeanName); + } + } + + PropertyConfig config = getPropertyConfig(property); + if ((property instanceof org.grails.datastore.mapping.model.types.OneToOne) && !isComposite) { + manyToOne.setAlternateUniqueKey(true); + Column c = getColumnForSimpleValue(manyToOne); + if (config != null && !config.isUniqueWithinGroup()) { + c.setUnique(config.isUnique()); + } + else if (property.isBidirectional() && isHasOne(property.getInverseSide())) { + c.setUnique(true); + } + } + } + + protected void bindCompositeIdentifierToManyToOne(Association property, + SimpleValue value, CompositeIdentity compositeId, PersistentEntity refDomainClass, + String path, String sessionFactoryBeanName) { + + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + + String[] propertyNames = compositeId.getPropertyNames(); + PropertyConfig config = getPropertyConfig(property); + + List columns = config.getColumns(); + int i = columns.size(); + int expectedForeignKeyColumnLength = calculateForeignKeyColumnCount(refDomainClass, propertyNames); + if (i != expectedForeignKeyColumnLength) { + int j = 0; + for (String propertyName : propertyNames) { + ColumnConfig cc; + // if a column configuration exists in the mapping use it + if (j < i) { + cc = columns.get(j++); + } + // otherwise create a new one to represent the composite column + else { + cc = new ColumnConfig(); + } + // if the name is null then configure the name by convention + if (cc.getName() == null) { + // use the referenced table name as a prefix + String prefix = getTableName(refDomainClass, sessionFactoryBeanName); + PersistentProperty referencedProperty = refDomainClass.getPropertyByName(propertyName); + + // if the referenced property is a ToOne and it has a composite id + // then a column is needed for each property that forms the composite id + if (referencedProperty instanceof ToOne) { + ToOne toOne = (ToOne) referencedProperty; + PersistentProperty[] compositeIdentity = toOne.getAssociatedEntity().getCompositeIdentity(); + if (compositeIdentity != null) { + for (PersistentProperty cip : compositeIdentity) { + // for each property of a composite id by default we use the table name and the property name as a prefix + String compositeIdPrefix = addUnderscore(prefix, namingStrategy.propertyToColumnName(referencedProperty.getName())); + String suffix = getDefaultColumnName(cip, sessionFactoryBeanName); + String finalColumnName = addUnderscore(compositeIdPrefix, suffix); + cc = new ColumnConfig(); + cc.setName(finalColumnName); + columns.add(cc); + } + continue; + } + } + + String suffix = getDefaultColumnName(referencedProperty, sessionFactoryBeanName); + String finalColumnName = addUnderscore(prefix, suffix); + cc.setName(finalColumnName); + columns.add(cc); + } + } + } + bindSimpleValue(property, value, path, config, sessionFactoryBeanName); + } + + // each property may consist of one or many columns (due to composite ids) so in order to get the + // number of columns required for a column key we have to perform the calculation here + private int calculateForeignKeyColumnCount(PersistentEntity refDomainClass, String[] propertyNames) { + int expectedForeignKeyColumnLength = 0; + for (String propertyName : propertyNames) { + PersistentProperty referencedProperty = refDomainClass.getPropertyByName(propertyName); + if (referencedProperty instanceof ToOne) { + ToOne toOne = (ToOne) referencedProperty; + PersistentProperty[] compositeIdentity = toOne.getAssociatedEntity().getCompositeIdentity(); + if (compositeIdentity != null) { + expectedForeignKeyColumnLength += compositeIdentity.length; + } + else { + expectedForeignKeyColumnLength++; + } + } + else { + expectedForeignKeyColumnLength++; + } + } + return expectedForeignKeyColumnLength; + } + + protected boolean hasCompositeIdentifier(Mapping mapping) { + return mapping != null && (mapping.getIdentity() instanceof CompositeIdentity); + } + + protected void bindOneToOne(final org.grails.datastore.mapping.model.types.OneToOne property, OneToOne oneToOne, + String path, String sessionFactoryBeanName) { + PropertyConfig config = getPropertyConfig(property); + final Association otherSide = property.getInverseSide(); + + final boolean hasOne = isHasOne(otherSide); + oneToOne.setConstrained(hasOne); + oneToOne.setForeignKeyType(oneToOne.isConstrained() ? + ForeignKeyDirection.FROM_PARENT : + ForeignKeyDirection.TO_PARENT); + oneToOne.setAlternateUniqueKey(true); + + if (config != null && config.getFetchMode() != null) { + oneToOne.setFetchMode(config.getFetchMode()); + } + else { + oneToOne.setFetchMode(FetchMode.DEFAULT); + } + + oneToOne.setReferencedEntityName(otherSide.getOwner().getName()); + oneToOne.setPropertyName(property.getName()); + oneToOne.setReferenceToPrimaryKey(false); + + bindOneToOneInternal(property, oneToOne, path); + + if (hasOne) { + PropertyConfig pc = getPropertyConfig(property); + bindSimpleValue(property, oneToOne, path, pc, sessionFactoryBeanName); + } + else { + oneToOne.setReferencedPropertyName(otherSide.getName()); + } + } + + protected void bindOneToOneInternal(org.grails.datastore.mapping.model.types.OneToOne property, OneToOne oneToOne, String path) { + //no-op, for subclasses to extend + } + + /** + */ + protected void bindManyToOneValues(org.grails.datastore.mapping.model.types.Association property, ManyToOne manyToOne) { + PropertyConfig config = getPropertyConfig(property); + + if (config != null && config.getFetchMode() != null) { + manyToOne.setFetchMode(config.getFetchMode()); + } + else { + manyToOne.setFetchMode(FetchMode.DEFAULT); + } + + manyToOne.setLazy(getLaziness(property)); + + if (config != null) { + manyToOne.setIgnoreNotFound(config.getIgnoreNotFound()); + } + + // set referenced entity + manyToOne.setReferencedEntityName(property.getAssociatedEntity().getName()); + } + + protected void bindVersion(PersistentProperty version, RootClass entity, + InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + + if (version != null) { + + SimpleValue val = new SimpleValue(metadataBuildingContext, entity.getTable()); + + bindSimpleValue(version, null, val, EMPTY_PATH, mappings, sessionFactoryBeanName); + + if (val.isTypeSpecified()) { + if (!(val.getType() instanceof IntegerType || + val.getType() instanceof LongType || + val.getType() instanceof TimestampType)) { + LOG.warn("Invalid version class specified in " + version.getOwner().getName() + + "; must be one of [int, Integer, long, Long, Timestamp, Date]. Not mapping the version."); + return; + } + } + else { + val.setTypeName("version".equals(version.getName()) ? "integer" : "timestamp"); + } + Property prop = new Property(); + prop.setValue(val); + bindProperty(version, prop, mappings); + prop.setLazy(false); + val.setNullValue("undefined"); + entity.setVersion(prop); + entity.setDeclaredVersion(prop); + entity.setOptimisticLockStyle(OptimisticLockStyle.VERSION); + entity.addProperty(prop); + } + else { + entity.setOptimisticLockStyle(OptimisticLockStyle.NONE); + } + } + + @SuppressWarnings("unchecked") + protected void bindSimpleId(PersistentProperty identifier, RootClass entity, + InFlightMetadataCollector mappings, Identity mappedId, String sessionFactoryBeanName) { + + Mapping mapping = getMapping(identifier.getOwner()); + boolean useSequence = mapping != null && mapping.isTablePerConcreteClass(); + + // create the id value + SimpleValue id = new SimpleValue(metadataBuildingContext, entity.getTable()); + Property idProperty = new Property(); + idProperty.setName(identifier.getName()); + idProperty.setValue(id); + entity.setDeclaredIdentifierProperty(idProperty); + // set identifier on entity + + Properties params = new Properties(); + entity.setIdentifier(id); + + if (mappedId == null) { + // configure generator strategy + id.setIdentifierGeneratorStrategy(useSequence ? "sequence-identity" : "native"); + } else { + String generator = mappedId.getGenerator(); + if ("native".equals(generator) && useSequence) { + generator = "sequence-identity"; + } + id.setIdentifierGeneratorStrategy(generator); + params.putAll(mappedId.getParams()); + if (params.containsKey(SEQUENCE_KEY)) { + params.put(SequenceStyleGenerator.SEQUENCE_PARAM, params.getProperty(SEQUENCE_KEY)); + } + if ("assigned".equals(generator)) { + id.setNullValue("undefined"); + } + } + + String schemaName = getSchemaName(mappings); + String catalogName = getCatalogName(mappings); + + params.put(PersistentIdentifierGenerator.IDENTIFIER_NORMALIZER, this.metadataBuildingContext.getObjectNameNormalizer()); + + if (schemaName != null) { + params.setProperty(PersistentIdentifierGenerator.SCHEMA, schemaName); + } + if (catalogName != null) { + params.setProperty(PersistentIdentifierGenerator.CATALOG, catalogName); + } + id.setIdentifierGeneratorProperties(params); + + // bind value + bindSimpleValue(identifier, null, id, EMPTY_PATH, mappings, sessionFactoryBeanName); + + // create property + Property prop = new Property(); + prop.setValue(id); + + // bind property + bindProperty(identifier, prop, mappings); + // set identifier property + entity.setIdentifierProperty(prop); + + id.getTable().setIdentifierValue(id); + } + + private String getSchemaName(InFlightMetadataCollector mappings) { + Identifier schema = mappings.getDatabase().getDefaultNamespace().getName().getSchema(); + if (schema != null) { + return schema.getCanonicalName(); + } + return null; + } + + private String getCatalogName(InFlightMetadataCollector mappings) { + Identifier catalog = mappings.getDatabase().getDefaultNamespace().getName().getCatalog(); + if (catalog != null) { + return catalog.getCanonicalName(); + } + return null; + } + + /** + * Binds a property to Hibernate runtime meta model. Deals with cascade strategy based on the Grails domain model + * + * @param grailsProperty The grails property instance + * @param prop The Hibernate property + * @param mappings The Hibernate mappings + */ + protected void bindProperty(PersistentProperty grailsProperty, Property prop, InFlightMetadataCollector mappings) { + // set the property name + prop.setName(grailsProperty.getName()); + if (isBidirectionalManyToOneWithListMapping(grailsProperty, prop)) { + prop.setInsertable(false); + prop.setUpdateable(false); + } else { + prop.setInsertable(getInsertableness(grailsProperty)); + prop.setUpdateable(getUpdateableness(grailsProperty)); + } + + AccessType accessType = AccessType.getAccessStrategy( + grailsProperty.getMapping().getMappedForm().getAccessType() + ); + + if (accessType == AccessType.FIELD) { + EntityReflector.PropertyReader reader = grailsProperty.getReader(); + Method getter = reader != null ? reader.getter() : null; + if (getter != null && getter.getAnnotation(Traits.Implemented.class) != null) { + prop.setPropertyAccessorName(TraitPropertyAccessStrategy.class.getName()); + } + else { + prop.setPropertyAccessorName(accessType.getType()); + } + } + else { + prop.setPropertyAccessorName(accessType.getType()); + } + + prop.setOptional(grailsProperty.isNullable()); + + setCascadeBehaviour(grailsProperty, prop); + + // lazy to true + final boolean isToOne = grailsProperty instanceof ToOne && !(grailsProperty instanceof Embedded); + PersistentEntity propertyOwner = grailsProperty.getOwner(); + boolean isLazyable = isToOne || + !(grailsProperty instanceof Association) && !grailsProperty.equals(propertyOwner.getIdentity()); + + if (isLazyable) { + final boolean isLazy = getLaziness(grailsProperty); + prop.setLazy(isLazy); + + if (isLazy && isToOne && !(PersistentAttributeInterceptable.class.isAssignableFrom(propertyOwner.getJavaClass()))) { + // handleLazyProxy(propertyOwner, grailsProperty); + } + } + } + + protected boolean getLaziness(PersistentProperty grailsProperty) { + PropertyConfig config = getPropertyConfig(grailsProperty); + final Boolean lazy = config.getLazy(); + if (lazy == null && grailsProperty instanceof Association) { + return true; + } + else if (lazy != null) { + return lazy; + } + return false; + } + + protected boolean getInsertableness(PersistentProperty grailsProperty) { + PropertyConfig config = getPropertyConfig(grailsProperty); + return config == null || config.getInsertable(); + } + + protected boolean getUpdateableness(PersistentProperty grailsProperty) { + PropertyConfig config = getPropertyConfig(grailsProperty); + return config == null || config.getUpdatable(); + } + + protected boolean isBidirectionalManyToOneWithListMapping(PersistentProperty grailsProperty, Property prop) { + if (grailsProperty instanceof Association) { + + Association association = (Association) grailsProperty; + Association otherSide = association.getInverseSide(); + return association.isBidirectional() && otherSide != null && + prop.getValue() instanceof ManyToOne && + List.class.isAssignableFrom(otherSide.getType()); + } + return false; + } + + protected void setCascadeBehaviour(PersistentProperty grailsProperty, Property prop) { + String cascadeStrategy = "none"; + // set to cascade all for the moment + PersistentEntity domainClass = grailsProperty.getOwner(); + PropertyConfig config = getPropertyConfig(grailsProperty); + if (config != null && config.getCascade() != null) { + cascadeStrategy = config.getCascade(); + } else if (grailsProperty instanceof Association) { + Association association = (Association) grailsProperty; + PersistentEntity referenced = association.getAssociatedEntity(); + if (isHasOne(association)) { + cascadeStrategy = CASCADE_ALL; + } + else if (association instanceof org.grails.datastore.mapping.model.types.OneToOne) { + if (referenced != null && association.isOwningSide()) { + cascadeStrategy = CASCADE_ALL; + } + else { + cascadeStrategy = CASCADE_SAVE_UPDATE; + } + } else if (association instanceof org.grails.datastore.mapping.model.types.OneToMany) { + if (referenced != null && association.isOwningSide()) { + cascadeStrategy = CASCADE_ALL; + } + else { + cascadeStrategy = CASCADE_SAVE_UPDATE; + } + } else if (grailsProperty instanceof ManyToMany) { + if ((referenced != null && referenced.isOwningEntity(domainClass)) || association.isCircular()) { + cascadeStrategy = CASCADE_SAVE_UPDATE; + } + } else if (grailsProperty instanceof org.grails.datastore.mapping.model.types.ManyToOne) { + if (referenced != null && referenced.isOwningEntity(domainClass) && !isCircularAssociation(grailsProperty)) { + cascadeStrategy = CASCADE_ALL; + } + else if (isCompositeIdProperty((Mapping) domainClass.getMapping().getMappedForm(), grailsProperty)) { + cascadeStrategy = CASCADE_ALL; + } + else { + cascadeStrategy = CASCADE_NONE; + } + } + else if (grailsProperty instanceof Basic) { + cascadeStrategy = CASCADE_ALL; + } + else if (Map.class.isAssignableFrom(grailsProperty.getType())) { + referenced = association.getAssociatedEntity(); + if (referenced != null && referenced.isOwningEntity(domainClass)) { + cascadeStrategy = CASCADE_ALL; + } else { + cascadeStrategy = CASCADE_SAVE_UPDATE; + } + } + logCascadeMapping(association, cascadeStrategy, referenced); + } + prop.setCascade(cascadeStrategy); + } + + protected boolean isCircularAssociation(PersistentProperty grailsProperty) { + return grailsProperty.getType().equals(grailsProperty.getOwner().getJavaClass()); + } + + protected void logCascadeMapping(Association grailsProperty, String cascadeStrategy, PersistentEntity referenced) { + if (LOG.isDebugEnabled() & referenced != null) { + String assType = getAssociationDescription(grailsProperty); + LOG.debug("Mapping cascade strategy for " + assType + " property " + grailsProperty.getOwner().getName() + "." + grailsProperty.getName() + " referencing type [" + referenced.getJavaClass().getName() + "] -> [CASCADE: " + cascadeStrategy + "]"); + } + } + + protected String getAssociationDescription(Association grailsProperty) { + String assType = "unknown"; + if (grailsProperty instanceof ManyToMany) { + assType = "many-to-many"; + } else if (grailsProperty instanceof org.grails.datastore.mapping.model.types.OneToMany) { + assType = "one-to-many"; + } else if (grailsProperty instanceof org.grails.datastore.mapping.model.types.OneToOne) { + assType = "one-to-one"; + } else if (grailsProperty instanceof org.grails.datastore.mapping.model.types.ManyToOne) { + assType = "many-to-one"; + } else if (grailsProperty.isEmbedded()) { + assType = "embedded"; + } + return assType; + } + + /** + * Binds a simple value to the Hibernate metamodel. A simple value is + * any type within the Hibernate type system + * + * @param property + * @param parentProperty + * @param simpleValue The simple value to bind + * @param path + * @param mappings The Hibernate mappings instance + * @param sessionFactoryBeanName the session factory bean name + */ + protected void bindSimpleValue(PersistentProperty property, PersistentProperty parentProperty, + SimpleValue simpleValue, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { + // set type + bindSimpleValue(property, parentProperty, simpleValue, path, getPropertyConfig(property), sessionFactoryBeanName); + } + + protected void bindSimpleValue(PersistentProperty grailsProp, SimpleValue simpleValue, + String path, PropertyConfig propertyConfig, String sessionFactoryBeanName) { + bindSimpleValue(grailsProp, null, simpleValue, path, propertyConfig, sessionFactoryBeanName); + } + + protected void bindSimpleValue(PersistentProperty grailsProp, + PersistentProperty parentProperty, SimpleValue simpleValue, + String path, PropertyConfig propertyConfig, String sessionFactoryBeanName) { + setTypeForPropertyConfig(grailsProp, simpleValue, propertyConfig); + final PropertyConfig mappedForm = (PropertyConfig) grailsProp.getMapping().getMappedForm(); + if (mappedForm.isDerived() && !(grailsProp instanceof TenantId)) { + Formula formula = new Formula(); + formula.setFormula(propertyConfig.getFormula()); + simpleValue.addFormula(formula); + } else { + Table table = simpleValue.getTable(); + boolean hasConfig = propertyConfig != null; + + String generator = hasConfig ? propertyConfig.getGenerator() : null; + if (generator != null) { + simpleValue.setIdentifierGeneratorStrategy(generator); + Properties params = propertyConfig.getTypeParams(); + if (params != null) { + Properties generatorProps = new Properties(); + generatorProps.putAll(params); + + if (generatorProps.containsKey(SEQUENCE_KEY)) { + generatorProps.put(SequenceStyleGenerator.SEQUENCE_PARAM, generatorProps.getProperty(SEQUENCE_KEY)); + } + simpleValue.setIdentifierGeneratorProperties(generatorProps); + } + } + + // Add the column definitions for this value/property. Note that + // not all custom mapped properties will have column definitions, + // in which case we still need to create a Hibernate column for + // this value. + List columnDefinitions = hasConfig ? propertyConfig.getColumns() : + Arrays.asList(new Object[] { null }); + if (columnDefinitions.isEmpty()) { + columnDefinitions = Arrays.asList(new Object[] { null }); + } + + for (Object columnDefinition : columnDefinitions) { + ColumnConfig cc = (ColumnConfig) columnDefinition; + Column column = new Column(); + + // Check for explicitly mapped column name and SQL type. + if (cc != null) { + if (cc.getName() != null) { + column.setName(cc.getName()); + } + if (cc.getSqlType() != null) { + column.setSqlType(cc.getSqlType()); + } + } + + column.setValue(simpleValue); + + if (cc != null) { + if (cc.getLength() != -1) { + column.setLength(cc.getLength()); + } + if (cc.getPrecision() != -1) { + column.setPrecision(cc.getPrecision()); + } + if (cc.getScale() != -1) { + column.setScale(cc.getScale()); + } + if (!mappedForm.isUniqueWithinGroup()) { + column.setUnique(cc.isUnique()); + } + } + + bindColumn(grailsProp, parentProperty, column, cc, path, table, sessionFactoryBeanName); + + if (table != null) { + table.addColumn(column); + } + + simpleValue.addColumn(column); + } + } + } + + protected void setTypeForPropertyConfig(PersistentProperty grailsProp, SimpleValue simpleValue, PropertyConfig config) { + final String typeName = getTypeName(grailsProp, getPropertyConfig(grailsProp), getMapping(grailsProp.getOwner())); + if (typeName == null) { + simpleValue.setTypeName(grailsProp.getType().getName()); + } + else { + simpleValue.setTypeName(typeName); + if (config != null) { + simpleValue.setTypeParameters(config.getTypeParams()); + } + } + } + + /** + * Binds a value for the specified parameters to the meta model. + * + * @param type The type of the property + * @param simpleValue The simple value instance + * @param nullable Whether it is nullable + * @param columnName The property name + * @param mappings The mappings + */ + protected void bindSimpleValue(String type, SimpleValue simpleValue, boolean nullable, + String columnName, InFlightMetadataCollector mappings) { + + simpleValue.setTypeName(type); + Table t = simpleValue.getTable(); + Column column = new Column(); + column.setNullable(nullable); + column.setValue(simpleValue); + column.setName(columnName); + if (t != null) t.addColumn(column); + + simpleValue.addColumn(column); + } + + /** + * Binds a Column instance to the Hibernate meta model + * + * @param property The Grails domain class property + * @param parentProperty + * @param column The column to bind + * @param path + * @param table The table name + * @param sessionFactoryBeanName the session factory bean name + */ + protected void bindColumn(PersistentProperty property, PersistentProperty parentProperty, + Column column, ColumnConfig cc, String path, Table table, String sessionFactoryBeanName) { + + if (cc != null) { + column.setComment(cc.getComment()); + column.setDefaultValue(cc.getDefaultValue()); + column.setCustomRead(cc.getRead()); + column.setCustomWrite(cc.getWrite()); + } + + Class userType = getUserType(property); + String columnName = getColumnNameForPropertyAndPath(property, path, cc, sessionFactoryBeanName); + if ((property instanceof Association) && userType == null) { + Association association = (Association) property; + // Only use conventional naming when the column has not been explicitly mapped. + if (column.getName() == null) { + column.setName(columnName); + } + if (property instanceof ManyToMany) { + column.setNullable(false); + } + else if (property instanceof org.grails.datastore.mapping.model.types.OneToOne && association.isBidirectional() && !association.isOwningSide()) { + if (isHasOne(((Association) property).getInverseSide())) { + column.setNullable(false); + } + else { + column.setNullable(true); + } + } + else if ((property instanceof ToOne) && association.isCircular()) { + column.setNullable(true); + } + else { + column.setNullable(property.isNullable()); + } + } + else { + column.setName(columnName); + column.setNullable(property.isNullable() || (parentProperty != null && parentProperty.isNullable())); + + // Use the constraints for this property to more accurately define + // the column's length, precision, and scale + if (String.class.isAssignableFrom(property.getType()) || byte[].class.isAssignableFrom(property.getType())) { + bindStringColumnConstraints(column, property); + } + + if (Number.class.isAssignableFrom(property.getType())) { + bindNumericColumnConstraints(column, property, cc); + } + } + + handleUniqueConstraint(property, column, path, table, columnName, sessionFactoryBeanName); + + bindIndex(columnName, column, cc, table); + + final PersistentEntity owner = property.getOwner(); + if (!owner.isRoot()) { + Mapping mapping = getMapping(owner); + if (mapping == null || mapping.getTablePerHierarchy()) { + if (LOG.isDebugEnabled()) + LOG.debug("[GrailsDomainBinder] Sub class property [" + property.getName() + "] for column name [" + column.getName() + "] set to nullable"); + column.setNullable(true); + } else { + column.setNullable(property.isNullable()); + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("[GrailsDomainBinder] bound property [" + property.getName() + "] to column name [" + column.getName() + "] in table [" + table.getName() + "]"); + } + + protected void createKeyForProps(PersistentProperty grailsProp, String path, Table table, + String columnName, List propertyNames, String sessionFactoryBeanName) { + List keyList = new ArrayList<>(); + keyList.add(new Column(columnName)); + for (Iterator i = propertyNames.iterator(); i.hasNext();) { + String propertyName = (String) i.next(); + PersistentProperty otherProp = grailsProp.getOwner().getPropertyByName(propertyName); + if (otherProp == null) { + throw new MappingException(grailsProp.getOwner().getJavaClass().getName() + " references an unknown property " + propertyName); + } + String otherColumnName = getColumnNameForPropertyAndPath(otherProp, path, null, sessionFactoryBeanName); + keyList.add(new Column(otherColumnName)); + } + createUniqueKeyForColumns(table, columnName, keyList); + } + + protected void createUniqueKeyForColumns(Table table, String columnName, List columns) { + Collections.reverse(columns); + + UniqueKey uk = new UniqueKey(); + uk.setTable(table); + uk.addColumns(columns.iterator()); + + if (LOG.isDebugEnabled()) { + LOG.debug("create unique key for " + table.getName() + " columns = " + columns); + } + setGeneratedUniqueName(uk); + table.addUniqueKey(uk); + } + + protected void bindIndex(String columnName, Column column, ColumnConfig cc, Table table) { + if (cc == null) { + return; + } + + Object indexObj = cc.getIndex(); + String indexDefinition = null; + if (indexObj instanceof Boolean) { + Boolean b = (Boolean) indexObj; + if (b) { + indexDefinition = table.getName() + '_' + columnName + "_idx"; + } + } + else if (indexObj != null) { + indexDefinition = indexObj.toString(); + } + if (indexDefinition == null) { + return; + } + + String[] tokens = indexDefinition.split(","); + for (String index : tokens) { + table.getOrCreateIndex(index).addColumn(column); + } + } + + protected String getColumnNameForPropertyAndPath(PersistentProperty grailsProp, + String path, ColumnConfig cc, String sessionFactoryBeanName) { + + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + + // First try the column config. + String columnName = null; + if (cc == null) { + // No column config given, so try to fetch it from the mapping + PersistentEntity domainClass = grailsProp.getOwner(); + Mapping m = getMapping(domainClass); + if (m != null) { + PropertyConfig c = m.getPropertyConfig(grailsProp.getName()); + + if (supportsJoinColumnMapping(grailsProp) && hasJoinKeyMapping(c)) { + columnName = c.getJoinTable().getKey().getName(); + } + else if (c != null && c.getColumn() != null) { + columnName = c.getColumn(); + } + } + } + else { + if (supportsJoinColumnMapping(grailsProp)) { + PropertyConfig pc = getPropertyConfig(grailsProp); + if (hasJoinKeyMapping(pc)) { + columnName = pc.getJoinTable().getKey().getName(); + } + else { + columnName = cc.getName(); + } + } + else { + columnName = cc.getName(); + } + } + + if (columnName == null) { + if (isNotEmpty(path)) { + columnName = addUnderscore(namingStrategy.propertyToColumnName(path), + getDefaultColumnName(grailsProp, sessionFactoryBeanName)); + } else { + columnName = getDefaultColumnName(grailsProp, sessionFactoryBeanName); + } + } + return columnName; + } + + protected boolean hasJoinKeyMapping(PropertyConfig c) { + return c != null && c.getJoinTable() != null && c.getJoinTable().getKey() != null; + } + + protected boolean supportsJoinColumnMapping(PersistentProperty grailsProp) { + return grailsProp instanceof ManyToMany || isUnidirectionalOneToMany(grailsProp) || grailsProp instanceof Basic; + } + + protected String getDefaultColumnName(PersistentProperty property, String sessionFactoryBeanName) { + + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + + String columnName = namingStrategy.propertyToColumnName(property.getName()); + if (property instanceof Association) { + Association association = (Association) property; + boolean isBasic = property instanceof Basic; + if (isBasic && ((PropertyConfig) property.getMapping().getMappedForm()).getType() != null) { + return columnName; + } + + if (isBasic) { + return getForeignKeyForPropertyDomainClass(property, sessionFactoryBeanName); + } + + if (property instanceof ManyToMany) { + return getForeignKeyForPropertyDomainClass(property, sessionFactoryBeanName); + } + + if (!association.isBidirectional() && association instanceof org.grails.datastore.mapping.model.types.OneToMany) { + String prefix = namingStrategy.classToTableName(property.getOwner().getName()); + return addUnderscore(prefix, columnName) + FOREIGN_KEY_SUFFIX; + } + + if (property.isInherited() && isBidirectionalManyToOne(property)) { + return namingStrategy.propertyToColumnName(property.getOwner().getName()) + '_' + columnName + FOREIGN_KEY_SUFFIX; + } + + return columnName + FOREIGN_KEY_SUFFIX; + } + + return columnName; + } + + protected String getForeignKeyForPropertyDomainClass(PersistentProperty property, + String sessionFactoryBeanName) { + final String propertyName = NameUtils.decapitalize(property.getOwner().getName()); + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + return namingStrategy.propertyToColumnName(propertyName) + FOREIGN_KEY_SUFFIX; + } + + protected String getIndexColumnName(PersistentProperty property, String sessionFactoryBeanName) { + PropertyConfig pc = getPropertyConfig(property); + if (pc != null && pc.getIndexColumn() != null && pc.getIndexColumn().getColumn() != null) { + return pc.getIndexColumn().getColumn(); + } + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + return namingStrategy.propertyToColumnName(property.getName()) + UNDERSCORE + IndexedCollection.DEFAULT_INDEX_COLUMN_NAME; + } + + protected String getIndexColumnType(PersistentProperty property, String defaultType) { + PropertyConfig pc = getPropertyConfig(property); + if (pc != null && pc.getIndexColumn() != null && pc.getIndexColumn().getType() != null) { + return getTypeName(property, pc.getIndexColumn(), getMapping(property.getOwner())); + } + return defaultType; + } + + protected String getMapElementName(PersistentProperty property, String sessionFactoryBeanName) { + PropertyConfig pc = getPropertyConfig(property); + + if (hasJoinTableColumnNameMapping(pc)) { + return pc.getJoinTable().getColumn().getName(); + } + + NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); + return namingStrategy.propertyToColumnName(property.getName()) + UNDERSCORE + IndexedCollection.DEFAULT_ELEMENT_COLUMN_NAME; + } + + protected boolean hasJoinTableColumnNameMapping(PropertyConfig pc) { + return pc != null && pc.getJoinTable() != null && pc.getJoinTable().getColumn() != null && pc.getJoinTable().getColumn().getName() != null; + } + + /** + * Interrogates the specified constraints looking for any constraints that would limit the + * length of the property's value. If such constraints exist, this method adjusts the length + * of the column accordingly. + * @param column the column that corresponds to the property + * @param constrainedProperty the property's constraints + */ + protected void bindStringColumnConstraints(Column column, PersistentProperty constrainedProperty) { + final org.grails.datastore.mapping.config.Property mappedForm = constrainedProperty.getMapping().getMappedForm(); + Number columnLength = mappedForm.getMaxSize(); + List inListValues = mappedForm.getInList(); + if (columnLength != null) { + column.setLength(columnLength.intValue()); + } else if (inListValues != null) { + column.setLength(getMaxSize(inListValues)); + } + } + + protected void bindNumericColumnConstraints(Column column, PersistentProperty constrainedProperty) { + bindNumericColumnConstraints(column, constrainedProperty, null); + } + + /** + * Interrogates the specified constraints looking for any constraints that would limit the + * precision and/or scale of the property's value. If such constraints exist, this method adjusts + * the precision and/or scale of the column accordingly. + * @param column the column that corresponds to the property + * @param property the property's constraints + * @param cc the column configuration + */ + protected void bindNumericColumnConstraints(Column column, PersistentProperty property, ColumnConfig cc) { + int scale = Column.DEFAULT_SCALE; + int precision = Column.DEFAULT_PRECISION; + + PropertyConfig constrainedProperty = (PropertyConfig) property.getMapping().getMappedForm(); + if (cc != null && cc.getScale() > -1) { + column.setScale(cc.getScale()); + } else if (constrainedProperty.getScale() > -1) { + scale = constrainedProperty.getScale(); + column.setScale(scale); + } + + if (cc != null && cc.getPrecision() > -1) { + column.setPrecision(cc.getPrecision()); + } + else { + + Comparable minConstraintValue = constrainedProperty.getMin(); + Comparable maxConstraintValue = constrainedProperty.getMax(); + + int minConstraintValueLength = 0; + if ((minConstraintValue != null) && (minConstraintValue instanceof Number)) { + minConstraintValueLength = Math.max( + countDigits((Number) minConstraintValue), + countDigits(((Number) minConstraintValue).longValue()) + scale); + } + int maxConstraintValueLength = 0; + if ((maxConstraintValue != null) && (maxConstraintValue instanceof Number)) { + maxConstraintValueLength = Math.max( + countDigits((Number) maxConstraintValue), + countDigits(((Number) maxConstraintValue).longValue()) + scale); + } + + if (minConstraintValueLength > 0 && maxConstraintValueLength > 0) { + // If both of min and max constraints are setted we could use + // maximum digits number in it as precision + precision = Math.max(minConstraintValueLength, maxConstraintValueLength); + } else { + // Overwise we should also use default precision + precision = DefaultGroovyMethods.max(new Integer[]{precision, minConstraintValueLength, maxConstraintValueLength}); + } + + column.setPrecision(precision); + } + } + + /** + * @return a count of the digits in the specified number + */ + protected int countDigits(Number number) { + int numDigits = 0; + + if (number != null) { + // Remove everything that's not a digit (e.g., decimal points or signs) + String digitsOnly = number.toString().replaceAll("\\D", EMPTY_PATH); + numDigits = digitsOnly.length(); + } + + return numDigits; + } + + /** + * @return the maximum length of the strings in the specified list + */ + protected int getMaxSize(List inListValues) { + int maxSize = 0; + + for (Object inListValue : inListValues) { + String value = (String) inListValue; + maxSize = Math.max(value.length(), maxSize); + } + + return maxSize; + } + + protected void handleUniqueConstraint(PersistentProperty property, Column column, String path, Table table, String columnName, String sessionFactoryBeanName) { + final PropertyConfig mappedForm = (PropertyConfig) property.getMapping().getMappedForm(); + if (mappedForm.isUnique()) { + if (!mappedForm.isUniqueWithinGroup()) { + column.setUnique(true); + } + else { + createKeyForProps(property, path, table, columnName, mappedForm.getUniquenessGroup(), sessionFactoryBeanName); + } + } + + } + + protected boolean isNotEmpty(String s) { + return GrailsHibernateUtil.isNotEmpty(s); + } + + protected String qualify(String prefix, String name) { + return GrailsHibernateUtil.qualify(prefix, name); + } + + protected String unqualify(String qualifiedName) { + return GrailsHibernateUtil.unqualify(qualifiedName); + } + + public MetadataBuildingContext getMetadataBuildingContext() { + return metadataBuildingContext; + } + + /** + * Second pass class for grails relationships. This is required as all + * persistent classes need to be loaded in the first pass and then relationships + * established in the second pass compile + * + * @author Graeme + */ + class GrailsCollectionSecondPass implements SecondPass { + + private static final long serialVersionUID = -5540526942092611348L; + + protected ToMany property; + protected InFlightMetadataCollector mappings; + protected Collection collection; + protected String sessionFactoryBeanName; + + public GrailsCollectionSecondPass(ToMany property, InFlightMetadataCollector mappings, + Collection coll, String sessionFactoryBeanName) { + this.property = property; + this.mappings = mappings; + this.collection = coll; + this.sessionFactoryBeanName = sessionFactoryBeanName; + } + + public void doSecondPass(Map persistentClasses, Map inheritedMetas) throws MappingException { + bindCollectionSecondPass(property, mappings, persistentClasses, collection, sessionFactoryBeanName); + createCollectionKeys(); + } + + protected void createCollectionKeys() { + collection.createAllKeys(); + + if (LOG.isDebugEnabled()) { + String msg = "Mapped collection key: " + columns(collection.getKey()); + if (collection.isIndexed()) + msg += ", index: " + columns(((IndexedCollection) collection).getIndex()); + if (collection.isOneToMany()) { + msg += ", one-to-many: " + + ((OneToMany) collection.getElement()).getReferencedEntityName(); + } else { + msg += ", element: " + columns(collection.getElement()); + } + LOG.debug(msg); + } + } + + protected String columns(Value val) { + StringBuilder columns = new StringBuilder(); + Iterator iter = val.getColumnIterator(); + while (iter.hasNext()) { + columns.append(((Selectable) iter.next()).getText()); + if (iter.hasNext()) columns.append(", "); + } + return columns.toString(); + } + + @SuppressWarnings("rawtypes") + public void doSecondPass(Map persistentClasses) throws MappingException { + bindCollectionSecondPass(property, mappings, persistentClasses, collection, sessionFactoryBeanName); + createCollectionKeys(); + } + } + + class ListSecondPass extends GrailsCollectionSecondPass { + private static final long serialVersionUID = -3024674993774205193L; + + public ListSecondPass(ToMany property, InFlightMetadataCollector mappings, + Collection coll, String sessionFactoryBeanName) { + super(property, mappings, coll, sessionFactoryBeanName); + } + + @Override + public void doSecondPass(Map persistentClasses, Map inheritedMetas) throws MappingException { + bindListSecondPass(property, mappings, persistentClasses, + (org.hibernate.mapping.List) collection, sessionFactoryBeanName); + } + + @SuppressWarnings("rawtypes") + @Override + public void doSecondPass(Map persistentClasses) throws MappingException { + bindListSecondPass(property, mappings, persistentClasses, + (org.hibernate.mapping.List) collection, sessionFactoryBeanName); + } + } + + class MapSecondPass extends GrailsCollectionSecondPass { + private static final long serialVersionUID = -3244991685626409031L; + + public MapSecondPass(ToMany property, InFlightMetadataCollector mappings, + Collection coll, String sessionFactoryBeanName) { + super(property, mappings, coll, sessionFactoryBeanName); + } + + @Override + public void doSecondPass(Map persistentClasses, Map inheritedMetas) throws MappingException { + bindMapSecondPass(property, mappings, persistentClasses, + (org.hibernate.mapping.Map) collection, sessionFactoryBeanName); + } + + @SuppressWarnings("rawtypes") + @Override + public void doSecondPass(Map persistentClasses) throws MappingException { + bindMapSecondPass(property, mappings, persistentClasses, + (org.hibernate.mapping.Map) collection, sessionFactoryBeanName); + } + } + + /** + * A Collection type, for the moment only Set is supported + * + * @author Graeme + */ + static abstract class CollectionType { + + protected final Class clazz; + protected final GrailsDomainBinder binder; + protected final MetadataBuildingContext buildingContext; + + protected CollectionType SET; + protected CollectionType LIST; + protected CollectionType BAG; + protected CollectionType MAP; + protected boolean initialized; + + protected final Map, CollectionType> INSTANCES = new HashMap<>(); + + public abstract Collection create(ToMany property, PersistentClass owner, + String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException; + + protected CollectionType(Class clazz, GrailsDomainBinder binder) { + this.clazz = clazz; + this.binder = binder; + this.buildingContext = binder.getMetadataBuildingContext(); + } + + @Override + public String toString() { + return clazz.getName(); + } + + protected void createInstances() { + + if (initialized) { + return; + } + + initialized = true; + + SET = new CollectionType(Set.class, binder) { + @Override + public Collection create(ToMany property, PersistentClass owner, + String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { + org.hibernate.mapping.Set coll = new org.hibernate.mapping.Set(buildingContext, owner); + coll.setCollectionTable(owner.getTable()); + coll.setTypeName(getTypeName(property)); + binder.bindCollection(property, coll, owner, mappings, path, sessionFactoryBeanName); + return coll; + } + }; + INSTANCES.put(Set.class, SET); + INSTANCES.put(SortedSet.class, SET); + + LIST = new CollectionType(List.class, binder) { + @Override + public Collection create(ToMany property, PersistentClass owner, + String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { + org.hibernate.mapping.List coll = new org.hibernate.mapping.List(buildingContext, owner); + coll.setCollectionTable(owner.getTable()); + coll.setTypeName(getTypeName(property)); + binder.bindCollection(property, coll, owner, mappings, path, sessionFactoryBeanName); + return coll; + } + }; + INSTANCES.put(List.class, LIST); + + BAG = new CollectionType(java.util.Collection.class, binder) { + @Override + public Collection create(ToMany property, PersistentClass owner, + String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { + Bag coll = new Bag(buildingContext, owner); + coll.setCollectionTable(owner.getTable()); + coll.setTypeName(getTypeName(property)); + binder.bindCollection(property, coll, owner, mappings, path, sessionFactoryBeanName); + return coll; + } + }; + INSTANCES.put(java.util.Collection.class, BAG); + + MAP = new CollectionType(Map.class, binder) { + @Override + public Collection create(ToMany property, PersistentClass owner, + String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { + org.hibernate.mapping.Map map = new org.hibernate.mapping.Map(buildingContext, owner); + map.setTypeName(getTypeName(property)); + binder.bindCollection(property, map, owner, mappings, path, sessionFactoryBeanName); + return map; + } + }; + INSTANCES.put(Map.class, MAP); + } + + public CollectionType collectionTypeForClass(Class clazz) { + createInstances(); + return INSTANCES.get(clazz); + } + + public String getTypeName(ToMany property) { + return binder.getTypeName(property, binder.getPropertyConfig(property), getMapping(property.getOwner())); + } + + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java new file mode 100644 index 00000000000..4ddea5c68d1 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java @@ -0,0 +1,464 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg; + +import java.util.List; +import java.util.Map; + +import groovy.lang.GroovyObject; +import groovy.lang.GroovySystem; +import groovy.lang.MetaClass; + +import org.hibernate.Criteria; +import org.hibernate.FetchMode; +import org.hibernate.FlushMode; +import org.hibernate.Hibernate; +import org.hibernate.LockMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.criterion.Order; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.internal.util.StringHelper; +import org.hibernate.proxy.HibernateProxy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.core.convert.ConversionService; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.config.GormProperties; +import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.model.types.Embedded; +import org.grails.datastore.mapping.reflect.ClassUtils; +import org.grails.orm.hibernate.AbstractHibernateDatastore; +import org.grails.orm.hibernate.datasource.MultipleDataSourceSupport; +import org.grails.orm.hibernate.proxy.HibernateProxyHandler; +import org.grails.orm.hibernate.support.HibernateRuntimeUtils; + +/** + * Utility methods for configuring Hibernate inside Grails. + * + * @author Graeme Rocher + * @since 0.4 + */ +public class GrailsHibernateUtil extends HibernateRuntimeUtils { + protected static final Logger LOG = LoggerFactory.getLogger(GrailsHibernateUtil.class); + + public static final String ARGUMENT_FETCH_SIZE = "fetchSize"; + public static final String ARGUMENT_TIMEOUT = "timeout"; + public static final String ARGUMENT_READ_ONLY = "readOnly"; + public static final String ARGUMENT_FLUSH_MODE = "flushMode"; + public static final String ARGUMENT_MAX = "max"; + public static final String ARGUMENT_OFFSET = "offset"; + public static final String ARGUMENT_ORDER = "order"; + public static final String ARGUMENT_SORT = "sort"; + public static final String ORDER_DESC = "desc"; + public static final String ORDER_ASC = "asc"; + public static final String ARGUMENT_FETCH = "fetch"; + public static final String ARGUMENT_IGNORE_CASE = "ignoreCase"; + public static final String ARGUMENT_CACHE = "cache"; + public static final String ARGUMENT_LOCK = "lock"; + public static final Class[] EMPTY_CLASS_ARRAY = {}; + + private static HibernateProxyHandler proxyHandler = new HibernateProxyHandler(); + + public static void populateArgumentsForCriteria(AbstractHibernateDatastore datastore, Class targetClass, Criteria c, Map argMap, ConversionService conversionService) { + populateArgumentsForCriteria(datastore, targetClass, c, argMap, conversionService, true); + } + + /** + * Populates criteria arguments for the given target class and arguments map + * + * @param datastore the GrailsApplication instance + * @param targetClass The target class + * @param c The criteria instance + * @param argMap The arguments map + */ + @SuppressWarnings("rawtypes") + public static void populateArgumentsForCriteria(AbstractHibernateDatastore datastore, Class targetClass, Criteria c, Map argMap, ConversionService conversionService, boolean useDefaultMapping) { + Integer maxParam = null; + Integer offsetParam = null; + if (argMap.containsKey(ARGUMENT_MAX)) { + maxParam = conversionService.convert(argMap.get(ARGUMENT_MAX), Integer.class); + } + if (argMap.containsKey(ARGUMENT_OFFSET)) { + offsetParam = conversionService.convert(argMap.get(ARGUMENT_OFFSET), Integer.class); + } + if (argMap.containsKey(ARGUMENT_FETCH_SIZE)) { + c.setFetchSize(conversionService.convert(argMap.get(ARGUMENT_FETCH_SIZE), Integer.class)); + } + if (argMap.containsKey(ARGUMENT_TIMEOUT)) { + c.setTimeout(conversionService.convert(argMap.get(ARGUMENT_TIMEOUT), Integer.class)); + } + if (argMap.containsKey(ARGUMENT_FLUSH_MODE)) { + c.setFlushMode(convertFlushMode(argMap.get(ARGUMENT_FLUSH_MODE))); + } + if (argMap.containsKey(ARGUMENT_READ_ONLY)) { + c.setReadOnly(ClassUtils.getBooleanFromMap(ARGUMENT_READ_ONLY, argMap)); + } + String orderParam = (String) argMap.get(ARGUMENT_ORDER); + Object fetchObj = argMap.get(ARGUMENT_FETCH); + if (fetchObj instanceof Map) { + Map fetch = (Map) fetchObj; + for (Object o : fetch.keySet()) { + String associationName = (String) o; + c.setFetchMode(associationName, getFetchMode(fetch.get(associationName))); + } + } + + final int max = maxParam == null ? -1 : maxParam; + final int offset = offsetParam == null ? -1 : offsetParam; + if (max > -1) { + c.setMaxResults(max); + } + if (offset > -1) { + c.setFirstResult(offset); + } + if (ClassUtils.getBooleanFromMap(ARGUMENT_LOCK, argMap)) { + c.setLockMode(LockMode.PESSIMISTIC_WRITE); + c.setCacheable(false); + } + else { + if (argMap.containsKey(ARGUMENT_CACHE)) { + c.setCacheable(ClassUtils.getBooleanFromMap(ARGUMENT_CACHE, argMap)); + } else { + cacheCriteriaByMapping(targetClass, c); + } + } + + final Object sortObj = argMap.get(ARGUMENT_SORT); + if (sortObj != null) { + boolean ignoreCase = true; + Object caseArg = argMap.get(ARGUMENT_IGNORE_CASE); + if (caseArg instanceof Boolean) { + ignoreCase = (Boolean) caseArg; + } + if (sortObj instanceof Map) { + Map sortMap = (Map) sortObj; + for (Object sort : sortMap.keySet()) { + final String order = ORDER_DESC.equalsIgnoreCase((String) sortMap.get(sort)) ? ORDER_DESC : ORDER_ASC; + addOrderPossiblyNested(datastore, c, targetClass, (String) sort, order, ignoreCase); + } + } else { + final String sort = (String) sortObj; + final String order = ORDER_DESC.equalsIgnoreCase(orderParam) ? ORDER_DESC : ORDER_ASC; + addOrderPossiblyNested(datastore, c, targetClass, sort, order, ignoreCase); + } + } + else if (useDefaultMapping) { + Mapping m = GrailsDomainBinder.getMapping(targetClass); + if (m != null) { + Map sortMap = m.getSort().getNamesAndDirections(); + for (Object sort : sortMap.keySet()) { + final String order = ORDER_DESC.equalsIgnoreCase((String) sortMap.get(sort)) ? ORDER_DESC : ORDER_ASC; + addOrderPossiblyNested(datastore, c, targetClass, (String) sort, order, true); + } + } + } + } + + /** + * @deprecated No replacement. Do not use. + */ + @Deprecated + public static void setBinder(GrailsDomainBinder binder) { + } + + /** + * Populates criteria arguments for the given target class and arguments map + * + * @param targetClass The target class + * @param c The criteria instance + * @param argMap The arguments map + * + */ + @Deprecated + @SuppressWarnings("rawtypes") + public static void populateArgumentsForCriteria(Class targetClass, Criteria c, Map argMap, ConversionService conversionService) { + populateArgumentsForCriteria(null, targetClass, c, argMap, conversionService); + } + + @SuppressWarnings("rawtypes") + public static void populateArgumentsForCriteria(Criteria c, Map argMap, ConversionService conversionService) { + populateArgumentsForCriteria(null, null, c, argMap, conversionService); + } + + private static FlushMode convertFlushMode(Object object) { + if (object == null) { + return null; + } + if (object instanceof FlushMode) { + return (FlushMode) object; + } + return FlushMode.valueOf(String.valueOf(object)); + } + + /** + * Add order to criteria, creating necessary subCriteria if nested sort property (ie. sort:'nested.property'). + */ + private static void addOrderPossiblyNested(AbstractHibernateDatastore datastore, Criteria c, Class targetClass, String sort, String order, boolean ignoreCase) { + int firstDotPos = sort.indexOf("."); + if (firstDotPos == -1) { + addOrder(c, sort, order, ignoreCase); + } else { // nested property + String sortHead = sort.substring(0, firstDotPos); + String sortTail = sort.substring(firstDotPos + 1); + PersistentProperty property = getGrailsDomainClassProperty(datastore, targetClass, sortHead); + if (property instanceof Embedded) { + // embedded objects cannot reference entities (at time of writing), so no more recursion needed + addOrder(c, sort, order, ignoreCase); + } else if (property instanceof Association) { + Criteria subCriteria = c.createCriteria(sortHead); + Class propertyTargetClass = ((Association) property).getAssociatedEntity().getJavaClass(); + GrailsHibernateUtil.cacheCriteriaByMapping(datastore, propertyTargetClass, subCriteria); + addOrderPossiblyNested(datastore, subCriteria, propertyTargetClass, sortTail, order, ignoreCase); // Recurse on nested sort + } + } + } + + /** + * Add order directly to criteria. + */ + private static void addOrder(Criteria c, String sort, String order, boolean ignoreCase) { + if (ORDER_DESC.equals(order)) { + c.addOrder(ignoreCase ? Order.desc(sort).ignoreCase() : Order.desc(sort)); + } + else { + c.addOrder(ignoreCase ? Order.asc(sort).ignoreCase() : Order.asc(sort)); + } + } + + /** + * Get hold of the GrailsDomainClassProperty represented by the targetClass' propertyName, + * assuming targetClass corresponds to a GrailsDomainClass. + */ + private static PersistentProperty getGrailsDomainClassProperty(AbstractHibernateDatastore datastore, Class targetClass, String propertyName) { + PersistentEntity grailsClass = datastore != null ? datastore.getMappingContext().getPersistentEntity(targetClass.getName()) : null; + if (grailsClass == null) { + throw new IllegalArgumentException("Unexpected: class is not a domain class:" + targetClass.getName()); + } + return grailsClass.getPropertyByName(propertyName); + } + + /** + * Configures the criteria instance to cache based on the configured mapping. + * + * @param targetClass The target class + * @param criteria The criteria + */ + public static void cacheCriteriaByMapping(Class targetClass, Criteria criteria) { + Mapping m = GrailsDomainBinder.getMapping(targetClass); + if (m != null && m.getCache() != null && m.getCache().getEnabled()) { + criteria.setCacheable(true); + } + } + + public static void cacheCriteriaByMapping(AbstractHibernateDatastore datastore, Class targetClass, Criteria criteria) { + cacheCriteriaByMapping(targetClass, criteria); + } + + /** + * Retrieves the fetch mode for the specified instance; otherwise returns the default FetchMode. + * + * @param object The object, converted to a string + * @return The FetchMode + */ + public static FetchMode getFetchMode(Object object) { + String name = object != null ? object.toString() : "default"; + if (name.equalsIgnoreCase(FetchMode.JOIN.toString()) || name.equalsIgnoreCase("eager")) { + return FetchMode.JOIN; + } + if (name.equalsIgnoreCase(FetchMode.SELECT.toString()) || name.equalsIgnoreCase("lazy")) { + return FetchMode.SELECT; + } + return FetchMode.DEFAULT; + } + + /** + * Sets the target object to read-only using the given SessionFactory instance. This + * avoids Hibernate performing any dirty checking on the object + * + * @see #setObjectToReadWrite(Object, org.hibernate.SessionFactory) + * + * @param target The target object + * @param sessionFactory The SessionFactory instance + */ + public static void setObjectToReadyOnly(Object target, SessionFactory sessionFactory) { + Object resource = TransactionSynchronizationManager.getResource(sessionFactory); + if (resource != null) { + Session session = sessionFactory.getCurrentSession(); + if (canModifyReadWriteState(session, target)) { + if (target instanceof HibernateProxy) { + target = ((HibernateProxy) target).getHibernateLazyInitializer().getImplementation(); + } + session.setReadOnly(target, true); + session.setHibernateFlushMode(FlushMode.MANUAL); + } + } + } + + private static boolean canModifyReadWriteState(Session session, Object target) { + return session.contains(target) && Hibernate.isInitialized(target); + } + + /** + * Sets the target object to read-write, allowing Hibernate to dirty check it and auto-flush changes. + * + * @see #setObjectToReadyOnly(Object, org.hibernate.SessionFactory) + * + * @param target The target object + * @param sessionFactory The SessionFactory instance + */ + public static void setObjectToReadWrite(final Object target, SessionFactory sessionFactory) { + Session session = sessionFactory.getCurrentSession(); + if (!canModifyReadWriteState(session, target)) { + return; + } + + SessionImplementor sessionImpl = (SessionImplementor) session; + EntityEntry ee = sessionImpl.getPersistenceContext().getEntry(target); + + if (ee == null || ee.getStatus() != Status.READ_ONLY) { + return; + } + + Object actualTarget = target; + if (target instanceof HibernateProxy) { + actualTarget = ((HibernateProxy) target).getHibernateLazyInitializer().getImplementation(); + } + + session.setReadOnly(actualTarget, false); + session.setHibernateFlushMode(FlushMode.AUTO); + incrementVersion(target); + } + + /** + * Increments the entities version number in order to force an update + * @param target The target entity + */ + public static void incrementVersion(Object target) { + MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(target.getClass()); + if (metaClass.hasProperty(target, GormProperties.VERSION) != null) { + Object version = metaClass.getProperty(target, GormProperties.VERSION); + if (version instanceof Long) { + Long newVersion = (Long) version + 1; + metaClass.setProperty(target, GormProperties.VERSION, newVersion); + } + } + } + + /** + * Ensures the meta class is correct for a given class + * + * @param target The GroovyObject + * @param persistentClass The persistent class + */ + @Deprecated + public static void ensureCorrectGroovyMetaClass(Object target, Class persistentClass) { + if (target instanceof GroovyObject) { + GroovyObject go = ((GroovyObject) target); + if (!go.getMetaClass().getTheClass().equals(persistentClass)) { + go.setMetaClass(GroovySystem.getMetaClassRegistry().getMetaClass(persistentClass)); + } + } + } + + /** + * Unwraps and initializes a HibernateProxy. + * @param proxy The proxy + * @return the unproxied instance + */ + public static Object unwrapProxy(HibernateProxy proxy) { + return proxyHandler.unwrap(proxy); + } + + /** + * Returns the proxy for a given association or null if it is not proxied + * + * @param obj The object + * @param associationName The named assoication + * @return A proxy + */ + public static HibernateProxy getAssociationProxy(Object obj, String associationName) { + return proxyHandler.getAssociationProxy(obj, associationName); + } + + /** + * Checks whether an associated property is initialized and returns true if it is + * + * @param obj The name of the object + * @param associationName The name of the association + * @return true if is initialized + */ + public static boolean isInitialized(Object obj, String associationName) { + return proxyHandler.isInitialized(obj, associationName); + } + + /** + * Unproxies a HibernateProxy. If the proxy is uninitialized, it automatically triggers an initialization. + * In case the supplied object is null or not a proxy, the object will be returned as-is. + */ + public static Object unwrapIfProxy(Object instance) { + return proxyHandler.unwrap(instance); + } + + /** + * @deprecated Use {@link MultipleDataSourceSupport#getDefaultDataSource(PersistentEntity)} instead + */ + @Deprecated + public static String getDefaultDataSource(PersistentEntity domainClass) { + return MultipleDataSourceSupport.getDefaultDataSource(domainClass); + } + + /** + * @deprecated Use {@link MultipleDataSourceSupport#getDatasourceNames(PersistentEntity)} instead + */ + @Deprecated + public static List getDatasourceNames(PersistentEntity domainClass) { + return MultipleDataSourceSupport.getDatasourceNames(domainClass); + } + + /** + * @deprecated Use {@link MultipleDataSourceSupport#getDefaultDataSource(PersistentEntity)} instead + */ + @Deprecated + public static boolean usesDatasource(PersistentEntity domainClass, String dataSourceName) { + return MultipleDataSourceSupport.usesDatasource(domainClass, dataSourceName); + } + + public static boolean isMappedWithHibernate(PersistentEntity domainClass) { + return domainClass instanceof HibernatePersistentEntity; + } + + public static String qualify(final String prefix, final String name) { + return StringHelper.qualify(prefix, name); + } + + public static boolean isNotEmpty(final String string) { + return StringHelper.isNotEmpty(string); + } + + public static String unqualify(final String qualifiedName) { + return StringHelper.unqualify(qualifiedName); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsIdentifierGeneratorFactory.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsIdentifierGeneratorFactory.java new file mode 100644 index 00000000000..67a5da6b001 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsIdentifierGeneratorFactory.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg; + +import java.lang.reflect.Field; + +import org.hibernate.cfg.Configuration; +import org.hibernate.id.SequenceGenerator; +import org.hibernate.id.factory.internal.DefaultIdentifierGeneratorFactory; + +import org.springframework.util.ReflectionUtils; + +/** + * Hibernate IdentifierGeneratorFactory that prefers sequence-identity generator over sequence generator + * + * @author Lari Hotari + */ +public class GrailsIdentifierGeneratorFactory extends DefaultIdentifierGeneratorFactory { + private static final long serialVersionUID = 1L; + + @Override + public Class getIdentifierGeneratorClass(String strategy) { + Class generatorClass = super.getIdentifierGeneratorClass(strategy); + if ("native".equals(strategy) && generatorClass == SequenceGenerator.class) { + generatorClass = super.getIdentifierGeneratorClass("sequence-identity"); + } + return generatorClass; + } + + public static void applyNewInstance(Configuration cfg) throws IllegalArgumentException, IllegalAccessException { + Field field = ReflectionUtils.findField(Configuration.class, "identifierGeneratorFactory"); + field.setAccessible(true); + field.set(cfg, new GrailsIdentifierGeneratorFactory()); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingBuilder.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingBuilder.groovy new file mode 100644 index 00000000000..b7d7e55b00a --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingBuilder.groovy @@ -0,0 +1,701 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileStatic + +import jakarta.persistence.AccessType + +import org.hibernate.FetchMode +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import org.grails.datastore.mapping.config.groovy.MappingConfigurationBuilder +import org.grails.datastore.mapping.model.config.GormProperties +import org.grails.datastore.mapping.reflect.ClassPropertyFetcher + +/** + * Implements the ORM mapping DSL constructing a model that can be evaluated by the + * GrailsDomainBinder class which maps GORM classes onto the database. + * + * @author Graeme Rocher + * @since 1.0 + */ + +class HibernateMappingBuilder implements MappingConfigurationBuilder { + + private static final String INCLUDE_PARAM = 'include' + private static final String EXCLUDE_PARAM = 'exclude' + static final Logger LOG = LoggerFactory.getLogger(this) + + Mapping mapping + final String className + final Closure defaultConstraints + + private List methodMissingExcludes = [] + private List methodMissingIncludes + + /** + * Constructor for builder + * + * @param className The name of the class being mapped + */ + HibernateMappingBuilder(String className) { + this.className = className + } + + HibernateMappingBuilder(Mapping mapping, String className, Closure defaultConstraints = null) { + this.mapping = mapping + this.className = className + this.defaultConstraints = defaultConstraints + } + + @Override + Map getProperties() { + return mapping.columns + } + + /** + * Central entry point for the class. Passing a closure that defines a set of mappings will evaluate said mappings + * and populate the "mapping" property of this class which can then be obtained with getMappings() + * + * @param mappingClosure The closure that defines the ORM DSL + */ + + @Override + @CompileStatic + Mapping evaluate(Closure mappingClosure, Object context = null) { + if (mapping == null) { + mapping = new Mapping() + } + mappingClosure.resolveStrategy = Closure.DELEGATE_ONLY + mappingClosure.delegate = this + try { + if (context != null) { + mappingClosure.call(context) + } + else { + mappingClosure.call() + } + } + finally { + mappingClosure.delegate = null + } + mapping + } + /** + * Include another config in this one + */ + @CompileStatic + void includes(Closure callable) { + if (!callable) { + return + } + + callable.resolveStrategy = Closure.DELEGATE_ONLY + callable.delegate = this + try { + callable.call() + } + finally { + callable.delegate = null + } + } + @CompileStatic + void hibernateCustomUserType(Map args) { + if (args.type && (args['class'] instanceof Class)) { + mapping.userTypes[args['class']] = args.type + } + } + + /** + *

Configures the table name. Example: + * { table 'foo' } + * + * @param name The name of the table + */ + @CompileStatic + void table(String name) { + mapping.tableName = name + } + + /** + *

Configures the discriminator name. Example: + * { discriminator 'foo' } + * + * @param name The name of the table + */ + @CompileStatic + void discriminator(String name) { + mapping.discriminator(name) + } + + /** + *

Configures the discriminator name. Example: + * { discriminator value:'foo', column:'type' } + * + * @param name The name of the table + */ + @CompileStatic + void discriminator(Map args) { + mapping.discriminator(args) + } + + /** + *

Configures whether to auto import packages domain classes in HQL queries. Default is true + * { autoImport false } + */ + @CompileStatic + void autoImport(boolean b) { + mapping.autoImport = b + } + + /** + *

Configures the table name. Example: + * { table name:'foo', schema:'dbo', catalog:'CRM' } + */ + @CompileStatic + void table(Map tableDef) { + mapping.table.name = tableDef?.name?.toString() + mapping.table.schema = tableDef?.schema?.toString() + mapping.table.catalog = tableDef?.catalog?.toString() + } + + /** + *

Configures the default sort column. Example: + * { sort 'foo' } + * + * @param name The name of the property to sort by + */ + void sort(String name) { + if (name) { + mapping.getSort().name = name + } + } + + void autowire(boolean autowire) { + mapping.autowire = autowire + } + + /** + * Whether to use dynamic update queries + */ + @CompileStatic + void dynamicUpdate(boolean b) { + mapping.dynamicUpdate = b + } + + /** + * Whether to use dynamic update queries + */ + @CompileStatic + void dynamicInsert(boolean b) { + mapping.dynamicInsert = b + } + + /** + *

Configures the default sort column. Example: + * { sort foo:'desc' } + * + * @param namesAndDirections The names and directions of the property to sort by + */ + void sort(Map namesAndDirections) { + if (namesAndDirections) { + mapping.getSort().namesAndDirections = namesAndDirections + } + } + + /** + * Configures the batch-size used for lazy loading + * @param num The batch size to use + */ + @CompileStatic + void batchSize(Integer num) { + if (num) { + mapping.batchSize = num + } + } + + /** + *

Configures the default sort direction. Example: + * { order 'desc' } + * + * @param name The name of the property to sort by + */ + void order(String direction) { + if ('desc'.equalsIgnoreCase(direction) || 'asc'.equalsIgnoreCase(direction)) { + mapping.getSort().direction = direction + } + } + + /** + * Set whether auto time stamping should occur for last_updated and date_created columns + */ + @CompileStatic + void autoTimestamp(boolean b) { + mapping.autoTimestamp = b + } + + /** + *

Configures whether to use versioning for optimistic locking + * { version false } + * + * @param isVersioned True if a version property should be configured + */ + @CompileStatic + void version(boolean isVersioned) { + mapping.version(isVersioned) + } + + /** + *

Configures the name of the version column + * { version 'foo' } + * + * @param isVersioned True if a version property should be configured + */ + @CompileStatic + void version(String versionColumn) { + mapping.version(versionColumn) + } + + /** + * Sets the tenant id + * + * @param tenantIdProperty The tenant id property + */ + void tenantId(String tenantIdProperty) { + mapping.tenantId(tenantIdProperty) + } + + /** + *

Configures the second-level cache for the class + * { cache usage:'read-only', include:'all' } + * + * @param args Named arguments that contain the "usage" and/or "include" parameters + */ + @CompileStatic + void cache(Map args) { + mapping.cache = new CacheConfig(enabled: true) + if (args.usage) { + if (CacheConfig.USAGE_OPTIONS.contains(args.usage)) { + mapping.cache.usage = args.usage + } + else { + LOG.warn('ORM Mapping Invalid: Specified [usage] with value [{}] of [cache] in class [{}] is not valid', args.usage, className) + } + } + if (args.include) { + if (CacheConfig.INCLUDE_OPTIONS.contains(args.include)) { + mapping.cache.include = args.include + } + else { + LOG.warn('ORM Mapping Invalid: Specified [include] with value [{}] of [cache] in class [{}] is not valid', args.include, className) + } + } + } + + /** + *

Configures the second-level cache for the class + * { cache 'read-only' } + * + * @param usage The usage type for the cache which is one of CacheConfig.USAGE_OPTIONS + */ + @CompileStatic + void cache(String usage) { + cache(usage: usage) + } + + /** + *

Configures the second-level cache for the class + * { cache 'read-only', include:'all } + * + * @param usage The usage type for the cache which is one of CacheConfig.USAGE_OPTIONS + */ + @CompileStatic + void cache(String usage, Map args) { + args = args ? args : [:] + args.usage = usage + cache(args) + } + + /** + * If true the class and its sub classes will be mapped with table per hierarchy mapping + */ + @CompileStatic + void tablePerHierarchy(boolean isTablePerHierarchy) { + mapping.tablePerHierarchy = isTablePerHierarchy + } + + /** + * If true the class and its subclasses will be mapped with table per subclass mapping + */ + @CompileStatic + void tablePerSubclass(boolean isTablePerSubClass) { + mapping.tablePerHierarchy = !isTablePerSubClass + } + + /** + * If true the class and its subclasses will be mapped with table per subclass mapping + */ + @CompileStatic + void tablePerConcreteClass(boolean isTablePerConcreteClass) { + if (isTablePerConcreteClass) { + mapping.tablePerHierarchy = false + mapping.tablePerConcreteClass = true + } + } + + /** + *

Configures the second-level cache with the default usage of 'read-write' and the default include of 'all' if + * the passed argument is true + * + * { cache true } + * + * @param shouldCache True if the default cache configuration should be applied + */ + @CompileStatic + void cache(boolean shouldCache) { + mapping.cache = new CacheConfig(enabled: shouldCache) + } + + /** + *

Configures the identity strategy for the mapping. Examples + * + * + * { id generator:'sequence' } + * { id composite: ['one', 'two'] } + * + * + * @param args The named arguments to the id method + */ + void id(Map args) { + if (args.composite) { + mapping.identity = new CompositeIdentity(propertyNames: args.composite as String[]) + if (args.compositeClass) { + mapping.identity.compositeClass = args.compositeClass + } + } + else { + if (args?.generator) { + mapping.identity.generator = args.remove('generator') + } + if (args?.name) { + mapping.identity.name = args.remove('name').toString() + } + if (args?.params) { + def params = args.remove('params') + for (entry in params) { + params[entry.key] = entry.value?.toString() + } + mapping.identity.params = params + } + if (args?.natural) { + def naturalArgs = args.remove('natural') + def propertyNames = naturalArgs instanceof Map ? naturalArgs.remove('properties') : naturalArgs + + if (propertyNames) { + def ni = new NaturalId() + ni.mutable = (naturalArgs instanceof Map) && naturalArgs.mutable ?: false + if (propertyNames instanceof List) { + ni.propertyNames = propertyNames + } + else { + ni.propertyNames = [propertyNames.toString()] + } + mapping.identity.natural = ni + } + } + // still more arguments? + if (args) { + handleMethodMissing('id', [args] as Object[]) + } + } + } + + /** + * A closure used by methodMissing to create column definitions + */ + private Closure handleMethodMissing = { String name, Object args -> + if (args && ((args[0] instanceof Map) || (args[0] instanceof Closure))) { + Map namedArgs = args[0] instanceof Map ? args[0] : [:] + + def newConfig = new PropertyConfig() + if (defaultConstraints != null && namedArgs.containsKey('shared')) { + PropertyConfig sharedConstraints = mapping.columns.get(namedArgs.shared) + if (sharedConstraints != null) { + newConfig = (PropertyConfig) sharedConstraints.clone() + } + } + else if (mapping.columns.containsKey('*')) { + // apply global constraints constraints + PropertyConfig globalConstraints = mapping.columns.get('*') + if (globalConstraints != null) { + newConfig = (PropertyConfig) globalConstraints.clone() + } + } + + PropertyConfig property = mapping.columns[name] ?: newConfig + property.name = namedArgs.name ?: property.name + property.generator = namedArgs.generator ?: property.generator + property.formula = namedArgs.formula ?: property.formula + property.accessType = namedArgs.accessType instanceof AccessType ? namedArgs.accessType : property.accessType + property.type = namedArgs.type ?: property.type + property.setLazy(namedArgs.lazy instanceof Boolean ? namedArgs.lazy : property.getLazy()) + property.insertable = namedArgs.insertable != null ? namedArgs.insertable : property.insertable + property.updatable = namedArgs.updateable != null ? namedArgs.updateable : property.updatable + property.updatable = namedArgs.updatable != null ? namedArgs.updatable : property.updatable + property.cascade = namedArgs.cascade ?: property.cascade + property.cascadeValidate = namedArgs.cascadeValidate != null ? namedArgs.cascadeValidate : property.cascadeValidate + property.sort = namedArgs.sort ?: property.sort + property.order = namedArgs.order ?: property.order + property.batchSize = namedArgs.batchSize instanceof Integer ? namedArgs.batchSize : property.batchSize + property.ignoreNotFound = namedArgs.ignoreNotFound instanceof Boolean ? namedArgs.ignoreNotFound : property.ignoreNotFound + property.typeParams = namedArgs.params ?: property.typeParams + property.setUnique(namedArgs.unique ? namedArgs.unique : property.unique) + property.nullable = namedArgs.nullable instanceof Boolean ? namedArgs.nullable : property.nullable + property.maxSize = namedArgs.maxSize instanceof Number ? namedArgs.maxSize : property.maxSize + property.minSize = namedArgs.minSize instanceof Number ? namedArgs.minSize : property.minSize + if (namedArgs.size instanceof IntRange) { + property.size = (IntRange) namedArgs.size + } + property.max = namedArgs.max instanceof Comparable ? namedArgs.max : property.max + property.min = namedArgs.min instanceof Comparable ? namedArgs.min : property.min + property.range = namedArgs.range instanceof ObjectRange ? namedArgs.range : null + property.inList = namedArgs.inList instanceof List ? namedArgs.inList : property.inList + + // Need to guard around calling getScale() for multi-column properties (issue #1048) + if (namedArgs.scale instanceof Integer) { + property.scale = (Integer) namedArgs.scale + } + + if (namedArgs.fetch) { + switch (namedArgs.fetch) { + case ~/(join|JOIN)/: + property.fetch = FetchMode.JOIN; break + case ~/(select|SELECT)/: + property.fetch = FetchMode.SELECT; break + default: + property.fetch = FetchMode.DEFAULT + } + } + + // Deal with any column configuration for this property. + if (args[-1] instanceof Closure) { + // Multiple column definitions for this property. + Closure c = args[-1] + c.delegate = new PropertyDefinitionDelegate(property) + c.resolveStrategy = Closure.DELEGATE_ONLY + c.call() + } + else { + // There is no sub-closure containing multiple column + // definitions, so pick up any column settings from + // the argument map. + ColumnConfig cc + if (property.columns) { + cc = property.columns[0] + } + else { + cc = new ColumnConfig() + property.columns << cc + } + + if (namedArgs['column']) cc.name = namedArgs['column'] + if (namedArgs['sqlType']) cc.sqlType = namedArgs['sqlType'] + if (namedArgs['enumType']) cc.enumType = namedArgs['enumType'] + if (namedArgs['index']) cc.index = namedArgs['index'] + if (namedArgs['unique']) cc.unique = namedArgs['unique'] + if (namedArgs['read']) cc.read = namedArgs['read'] + if (namedArgs['write']) cc.write = namedArgs['write'] + if (namedArgs.defaultValue) cc.defaultValue = namedArgs.defaultValue + if (namedArgs.comment) cc.comment = namedArgs.comment + cc.length = namedArgs['length'] ?: cc.length + cc.precision = namedArgs['precision'] ?: cc.precision + cc.scale = namedArgs['scale'] ?: cc.scale + } + + if (namedArgs.cache instanceof String) { + CacheConfig cc = new CacheConfig() + if (CacheConfig.USAGE_OPTIONS.contains(namedArgs.cache)) { + cc.usage = namedArgs.cache + } + else { + LOG.warn('ORM Mapping Invalid: Specified [usage] of [cache] with value [{}] for association [{}] in class [{}] is not valid', args.usage, name, className) + } + property.cache = cc + } + else if (namedArgs.cache == true) { + property.cache = new CacheConfig() + } + else if (namedArgs.cache instanceof Map) { + def cacheArgs = namedArgs.cache + CacheConfig cc = new CacheConfig() + if (CacheConfig.USAGE_OPTIONS.contains(cacheArgs.usage)) { + cc.usage = cacheArgs.usage + } + else { + LOG.warn('ORM Mapping Invalid: Specified [usage] of [cache] with value [{}] for association [{}] in class [{}] is not valid', args.usage, name, className) + } + if (CacheConfig.INCLUDE_OPTIONS.contains(cacheArgs.include)) { + cc.include = cacheArgs.include + } + else { + LOG.warn('ORM Mapping Invalid: Specified [include] of [cache] with value [{}] for association [{}] in class [{}] is not valid', args.include, name, className) + } + property.cache = cc + } + + if (namedArgs.indexColumn) { + def pc = new PropertyConfig() + property.indexColumn = pc + def cc = new ColumnConfig() + pc.columns << cc + def indexColArg = namedArgs.indexColumn + if (indexColArg instanceof Map) { + if (indexColArg.type) { + pc.type = indexColArg.remove('type') + } + bindArgumentsToColumnConfig(indexColArg, cc) + } + else { + cc.name = indexColArg.toString() + } + } + if (namedArgs.joinTable) { + def join = new JoinTable() + def joinArgs = namedArgs.joinTable + if (joinArgs instanceof String) { + join.name = joinArgs + } + else if (joinArgs instanceof Map) { + if (joinArgs.schema) join.schema = joinArgs.remove('schema') + if (joinArgs.catalog) join.catalog = joinArgs.remove('catalog') + if (joinArgs.name) join.name = joinArgs.remove('name') + if (joinArgs.key) { + join.key = new ColumnConfig(name: joinArgs.remove('key')) + } + if (joinArgs.column) { + ColumnConfig cc = new ColumnConfig(name: joinArgs.column) + join.column = cc + bindArgumentsToColumnConfig(joinArgs, cc) + } + } + property.joinTable = join + } + else if (namedArgs.containsKey('joinTable') && namedArgs.joinTable == false) { + property.joinTable = null + } + + mapping.columns[name] = property + } + } + + private bindArgumentsToColumnConfig(argMap, ColumnConfig cc) { + argMap.each { k, v -> + if (cc.metaClass.hasProperty(cc, k)) { + try { + cc."$k" = v + } + catch (Exception e) { + LOG.warn('Parameter [{}] cannot be used with [joinTable] argument', k.toString()) + } + } + } + } + + /** + *

Consumes the columns closure and populates the value into the Mapping objects columns property + * + * @param callable The closure containing the column definitions + */ + @CompileStatic + void columns(Closure callable) { + callable.resolveStrategy = Closure.DELEGATE_ONLY + callable.delegate = new Object() { + def invokeMethod(String methodName, Object args) { + handleMethodMissing.call(methodName, args) + } + } + callable.call() + } + + @CompileStatic + void datasource(String name) { + mapping.datasources = [name] + } + + @CompileStatic + void datasources(List names) { + mapping.datasources = names + } + + @CompileStatic + void comment(String comment) { + mapping.comment = comment + } + + void methodMissing(String name, Object args) { + if (methodMissingIncludes != null && !methodMissingIncludes.contains(name)) { + return + } + else if (methodMissingExcludes.contains(name)) { + return + } + + boolean hasArgs = args.asBoolean() + if ('user-type' == name && hasArgs && (args[0] instanceof Map)) { + hibernateCustomUserType(args[0]) + } + else if ('importFrom' == name && hasArgs && (args[0] instanceof Class)) { + // ignore, handled by constraints + List constraintsToImports = ClassPropertyFetcher.getStaticPropertyValuesFromInheritanceHierarchy((Class) args[0], GormProperties.CONSTRAINTS, Closure) + if (constraintsToImports) { + + List originalIncludes = this.methodMissingIncludes + List originalExludes = this.methodMissingExcludes + try { + if (args[-1] instanceof Map) { + Map argMap = (Map) args[-1] + def includes = argMap.get(INCLUDE_PARAM) + def excludes = argMap.get(EXCLUDE_PARAM) + if (includes instanceof List) { + this.methodMissingIncludes = includes + } + if (excludes instanceof List) { + this.methodMissingExcludes = excludes + } + } + + for (Closure callable in constraintsToImports) { + callable.setDelegate(this) + callable.setResolveStrategy(Closure.DELEGATE_ONLY) + callable.call() + } + } finally { + this.methodMissingIncludes = originalIncludes + this.methodMissingExcludes = originalExludes + } + } + } + else if (args && ((args[0] instanceof Map) || (args[0] instanceof Closure))) { + handleMethodMissing(name, args) + } + } +} + diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java new file mode 100644 index 00000000000..d69c5e234aa --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java @@ -0,0 +1,324 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg; + +import java.lang.annotation.Annotation; + +import groovy.lang.Closure; +import groovy.lang.GroovyObject; + +import org.springframework.validation.Errors; + +import grails.gorm.annotation.Entity; +import grails.gorm.hibernate.HibernateEntity; +import org.grails.datastore.gorm.GormEntity; +import org.grails.datastore.mapping.config.AbstractGormMappingFactory; +import org.grails.datastore.mapping.config.Property; +import org.grails.datastore.mapping.config.groovy.MappingConfigurationBuilder; +import org.grails.datastore.mapping.model.AbstractMappingContext; +import org.grails.datastore.mapping.model.ClassMapping; +import org.grails.datastore.mapping.model.DatastoreConfigurationException; +import org.grails.datastore.mapping.model.EmbeddedPersistentEntity; +import org.grails.datastore.mapping.model.IdentityMapping; +import org.grails.datastore.mapping.model.MappingConfigurationStrategy; +import org.grails.datastore.mapping.model.MappingContext; +import org.grails.datastore.mapping.model.MappingFactory; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.ValueGenerator; +import org.grails.datastore.mapping.model.config.GormProperties; +import org.grails.datastore.mapping.model.config.JpaMappingConfigurationStrategy; +import org.grails.datastore.mapping.reflect.ClassUtils; +import org.grails.orm.hibernate.connections.HibernateConnectionSourceSettings; +import org.grails.orm.hibernate.proxy.HibernateProxyHandler; + +/** + * A Mapping context for Hibernate + * + * @author Graeme Rocher + * @since 5.0 + */ +public class HibernateMappingContext extends AbstractMappingContext { + + private static final String[] DEFAULT_IDENTITY_MAPPING = new String[] {GormProperties.IDENTITY}; + private final HibernateMappingFactory mappingFactory; + private final MappingConfigurationStrategy syntaxStrategy; + + /** + * Construct a HibernateMappingContext for the given arguments + * + * @param settings The {@link HibernateConnectionSourceSettings} settings + * @param contextObject The context object (for example a Spring ApplicationContext) + * @param persistentClasses The persistent classes + */ + public HibernateMappingContext(HibernateConnectionSourceSettings settings, Object contextObject, Class... persistentClasses) { + this.mappingFactory = new HibernateMappingFactory(); + + // The mapping factory needs to be configured before initialize can be safely called + initialize(settings); + + if (settings != null) { + this.mappingFactory.setDefaultMapping(settings.getDefault().getMapping()); + this.mappingFactory.setDefaultConstraints(settings.getDefault().getConstraints()); + } + this.mappingFactory.setContextObject(contextObject); + this.syntaxStrategy = new JpaMappingConfigurationStrategy(mappingFactory) { + @Override + protected boolean supportsCustomType(Class propertyType) { + return !Errors.class.isAssignableFrom(propertyType); + } + }; + this.proxyFactory = new HibernateProxyHandler(); + addPersistentEntities(persistentClasses); + } + + public HibernateMappingContext(HibernateConnectionSourceSettings settings, Class... persistentClasses) { + this(settings, null, persistentClasses); + } + + public HibernateMappingContext() { + this(new HibernateConnectionSourceSettings()); + } + + /** + * Sets the default constraints to be used + * + * @param defaultConstraints The default constraints + */ + public void setDefaultConstraints(Closure defaultConstraints) { + this.mappingFactory.setDefaultConstraints(defaultConstraints); + } + + @Override + public MappingConfigurationStrategy getMappingSyntaxStrategy() { + return syntaxStrategy; + } + + @Override + public MappingFactory getMappingFactory() { + return mappingFactory; + } + + @Override + protected PersistentEntity createPersistentEntity(Class javaClass) { + if (GormEntity.class.isAssignableFrom(javaClass)) { + Object mappingStrategy = resolveMappingStrategy(javaClass); + if (isValidMappingStrategy(javaClass, mappingStrategy)) { + return new HibernatePersistentEntity(javaClass, this); + } + } + return null; + } + + @Override + protected boolean isValidMappingStrategy(Class javaClass, Object mappingStrategy) { + return HibernateEntity.class.isAssignableFrom(javaClass) || super.isValidMappingStrategy(javaClass, mappingStrategy); + } + + @Override + protected PersistentEntity createPersistentEntity(Class javaClass, boolean external) { + return createPersistentEntity(javaClass); + } + + public static boolean isDomainClass(Class clazz) { + return doIsDomainClassCheck(clazz); + } + + private static boolean doIsDomainClassCheck(Class clazz) { + if (GormEntity.class.isAssignableFrom(clazz)) { + return true; + } + + // it's not a closure + if (Closure.class.isAssignableFrom(clazz)) { + return false; + } + + if (clazz.isEnum()) return false; + + Annotation[] allAnnotations = clazz.getAnnotations(); + for (Annotation annotation : allAnnotations) { + Class type = annotation.annotationType(); + String annName = type.getName(); + if (annName.equals("grails.persistence.Entity")) { + return true; + } + if (type.equals(Entity.class)) { + return true; + } + } + + Class testClass = clazz; + while (testClass != null && !testClass.equals(GroovyObject.class) && !testClass.equals(Object.class)) { + try { + // make sure the identify and version field exist + testClass.getDeclaredField(GormProperties.IDENTITY); + testClass.getDeclaredField(GormProperties.VERSION); + + // passes all conditions return true + return true; + } + catch (SecurityException e) { + // ignore + } + catch (NoSuchFieldException e) { + // ignore + } + testClass = testClass.getSuperclass(); + } + + return false; + } + + @Override + public PersistentEntity createEmbeddedEntity(Class type) { + HibernateEmbeddedPersistentEntity embedded = new HibernateEmbeddedPersistentEntity(type, this); + embedded.initialize(); + return embedded; + } + + @Override + public PersistentEntity getPersistentEntity(String name) { + final int proxyIndicator = name.indexOf("$HibernateProxy$"); + if (proxyIndicator > -1) { + name = name.substring(0, proxyIndicator); + } + return super.getPersistentEntity(name); + } + + static class HibernateEmbeddedPersistentEntity extends EmbeddedPersistentEntity { + private final ClassMapping classMapping; + + public HibernateEmbeddedPersistentEntity(Class type, MappingContext ctx) { + super(type, ctx); + this.classMapping = new ClassMapping<>() { + Mapping mappedForm = (Mapping) context.getMappingFactory().createMappedForm(HibernateEmbeddedPersistentEntity.this); + + @Override + public PersistentEntity getEntity() { + return HibernateEmbeddedPersistentEntity.this; + } + + @Override + public Mapping getMappedForm() { + return mappedForm; + } + + @Override + public IdentityMapping getIdentifier() { + return null; + } + }; + } + + @Override + public ClassMapping getMapping() { + return classMapping; + } + } + + class HibernateMappingFactory extends AbstractGormMappingFactory { + + public HibernateMappingFactory() { + } + + @Override + protected MappingConfigurationBuilder createConfigurationBuilder(PersistentEntity entity, Mapping mapping) { + return new HibernateMappingBuilder(mapping, entity.getName(), defaultConstraints); + } + + @Override + public IdentityMapping createIdentityMapping(final ClassMapping classMapping) { + final Mapping mappedForm = createMappedForm(classMapping.getEntity()); + final Object identity = mappedForm.getIdentity(); + final ValueGenerator generator; + if (identity instanceof Identity) { + Identity id = (Identity) identity; + String generatorName = id.getGenerator(); + if (generatorName != null) { + ValueGenerator resolvedGenerator; + try { + resolvedGenerator = ValueGenerator.valueOf(generatorName.toUpperCase(java.util.Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + if (ClassUtils.isPresent(generatorName)) { + resolvedGenerator = ValueGenerator.CUSTOM; + } + else { + throw new DatastoreConfigurationException("Invalid id generation strategy for entity [" + classMapping.getEntity().getName() + "]: " + generatorName); + } + } + generator = resolvedGenerator; + } + else { + generator = ValueGenerator.AUTO; + } + } + else { + generator = ValueGenerator.AUTO; + } + return new IdentityMapping() { + @Override + public String[] getIdentifierName() { + if (identity instanceof Identity) { + final String name = ((Identity) identity).getName(); + if (name != null) { + return new String[]{name}; + } + else { + return DEFAULT_IDENTITY_MAPPING; + } + } + else if (identity instanceof CompositeIdentity) { + return ((CompositeIdentity) identity).getPropertyNames(); + } + return DEFAULT_IDENTITY_MAPPING; + } + + @Override + public ValueGenerator getGenerator() { + return generator; + } + + @Override + public ClassMapping getClassMapping() { + return classMapping; + } + + @Override + public Property getMappedForm() { + return (Property) identity; + } + }; + } + + @Override + protected boolean allowArbitraryCustomTypes() { + return true; + } + + @Override + protected Class getPropertyMappedFormType() { + return PropertyConfig.class; + } + + @Override + protected Class getEntityMappedFormType() { + return Mapping.class; + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java new file mode 100644 index 00000000000..16e1185d3f6 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java @@ -0,0 +1,378 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.cfg; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.sql.DataSource; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.MappedSuperclass; + +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.SessionFactory; +import org.hibernate.SessionFactoryObserver; +import org.hibernate.boot.registry.BootstrapServiceRegistry; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.boot.registry.selector.spi.StrategySelector; +import org.hibernate.boot.spi.MetadataContributor; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.Environment; +import org.hibernate.context.spi.CurrentSessionContext; +import org.hibernate.internal.util.config.ConfigurationHelper; +import org.hibernate.property.access.spi.PropertyAccessStrategy; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.service.spi.ServiceRegistryImplementor; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.util.ClassUtils; + +import org.grails.datastore.gorm.GormEntity; +import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings; +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.orm.hibernate.EventListenerIntegrator; +import org.grails.orm.hibernate.GrailsSessionContext; +import org.grails.orm.hibernate.HibernateEventListeners; +import org.grails.orm.hibernate.MetadataIntegrator; +import org.grails.orm.hibernate.access.TraitPropertyAccessStrategy; + +/** + * A Configuration that uses a MappingContext to configure Hibernate + * + * @since 5.0 + */ +public class HibernateMappingContextConfiguration extends Configuration implements ApplicationContextAware { + private static final long serialVersionUID = -7115087342689305517L; + + private static final String RESOURCE_PATTERN = "/**/*.class"; + + private static final TypeFilter[] ENTITY_TYPE_FILTERS = new TypeFilter[] { + new AnnotationTypeFilter(Entity.class, false), + new AnnotationTypeFilter(Embeddable.class, false), + new AnnotationTypeFilter(MappedSuperclass.class, false) + }; + + protected String sessionFactoryBeanName = "sessionFactory"; + protected String dataSourceName = ConnectionSource.DEFAULT; + protected HibernateMappingContext hibernateMappingContext; + private Class currentSessionContext = GrailsSessionContext.class; + private HibernateEventListeners hibernateEventListeners; + private Map eventListeners; + private ServiceRegistry serviceRegistry; + private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + private MetadataContributor metadataContributor; + private Set additionalClasses = new HashSet<>(); + + public void setHibernateMappingContext(HibernateMappingContext hibernateMappingContext) { + this.hibernateMappingContext = hibernateMappingContext; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(applicationContext); + String dsName = ConnectionSource.DEFAULT.equals(dataSourceName) ? "dataSource" : "dataSource_" + dataSourceName; + Properties properties = getProperties(); + + if (applicationContext.containsBean(dsName)) { + properties.put(Environment.DATASOURCE, applicationContext.getBean(dsName)); + } + properties.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, currentSessionContext.getName()); + properties.put(AvailableSettings.CLASSLOADERS, applicationContext.getClassLoader()); + } + + /** + * Set the target SQL {@link DataSource} + * + * @param connectionSource The data source to use + */ + public void setDataSourceConnectionSource(ConnectionSource connectionSource) { + this.dataSourceName = connectionSource.getName(); + DataSource source = connectionSource.getSource(); + getProperties().put(Environment.DATASOURCE, source); + getProperties().put(Environment.CURRENT_SESSION_CONTEXT_CLASS, GrailsSessionContext.class.getName()); + final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + if (contextClassLoader != null && contextClassLoader.getClass().getSimpleName().equalsIgnoreCase("RestartClassLoader")) { + getProperties().put(AvailableSettings.CLASSLOADERS, contextClassLoader); + } else { + getProperties().put(AvailableSettings.CLASSLOADERS, connectionSource.getClass().getClassLoader()); + } + } + + /** + * Add the given annotated classes in a batch. + * @see #addAnnotatedClass + * @see #scanPackages + */ + public void addAnnotatedClasses(Class... annotatedClasses) { + for (Class annotatedClass : annotatedClasses) { + addAnnotatedClass(annotatedClass); + } + } + + @Override + public Configuration addAnnotatedClass(Class annotatedClass) { + additionalClasses.add(annotatedClass); + return super.addAnnotatedClass(annotatedClass); + } + + /** + * Add the given annotated packages in a batch. + * @see #addPackage + * @see #scanPackages + */ + public void addPackages(String... annotatedPackages) { + for (String annotatedPackage :annotatedPackages) { + addPackage(annotatedPackage); + } + } + + /** + * Perform Spring-based scanning for entity classes, registering them + * as annotated classes with this {@code Configuration}. + * @param packagesToScan one or more Java package names + * @throws HibernateException if scanning fails for any reason + */ + public void scanPackages(String... packagesToScan) throws HibernateException { + try { + for (String pkg : packagesToScan) { + String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN; + Resource[] resources = resourcePatternResolver.getResources(pattern); + MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver); + for (Resource resource : resources) { + if (resource.isReadable()) { + MetadataReader reader = readerFactory.getMetadataReader(resource); + String className = reader.getClassMetadata().getClassName(); + if (matchesFilter(reader, readerFactory)) { + Class loadedClass = resourcePatternResolver.getClassLoader().loadClass(className); + addAnnotatedClasses(loadedClass); + } + } + } + } + } + catch (IOException ex) { + throw new MappingException("Failed to scan classpath for unlisted classes", ex); + } + catch (ClassNotFoundException ex) { + throw new MappingException("Failed to load annotated classes from classpath", ex); + } + } + + /** + * Check whether any of the configured entity type filters matches + * the current class descriptor contained in the metadata reader. + */ + protected boolean matchesFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException { + for (TypeFilter filter : ENTITY_TYPE_FILTERS) { + if (filter.match(reader, readerFactory)) { + return true; + } + } + return false; + } + + public void setSessionFactoryBeanName(String name) { + sessionFactoryBeanName = name; + } + + public void setDataSourceName(String name) { + dataSourceName = name; + } + + /* (non-Javadoc) + * @see org.hibernate.cfg.Configuration#buildSessionFactory() + */ + @Override + public SessionFactory buildSessionFactory() throws HibernateException { + + // set the class loader to load Groovy classes + + // work around for HHH-2624 + SessionFactory sessionFactory; + + Object classLoaderObject = getProperties().get(AvailableSettings.CLASSLOADERS); + ClassLoader appClassLoader; + + if (classLoaderObject instanceof ClassLoader) { + appClassLoader = (ClassLoader) classLoaderObject; + } + else { + appClassLoader = getClass().getClassLoader(); + } + + ConfigurationHelper.resolvePlaceHolders(getProperties()); + + final GrailsDomainBinder domainBinder = new GrailsDomainBinder( + dataSourceName, + sessionFactoryBeanName, + hibernateMappingContext + ); + + List annotatedClasses = new ArrayList<>(); + for (PersistentEntity persistentEntity : hibernateMappingContext.getPersistentEntities()) { + Class javaClass = persistentEntity.getJavaClass(); + if (javaClass.isAnnotationPresent(Entity.class)) { + annotatedClasses.add(javaClass); + } + } + + if (!additionalClasses.isEmpty()) { + for (Class additionalClass : additionalClasses) { + if (GormEntity.class.isAssignableFrom(additionalClass)) { + hibernateMappingContext.addPersistentEntity(additionalClass); + } + } + } + + addAnnotatedClasses(annotatedClasses.toArray(new Class[annotatedClasses.size()])); + + ClassLoaderService classLoaderService = new ClassLoaderServiceImpl(appClassLoader) { + @Override + public Collection loadJavaServices(Class serviceContract) { + if (MetadataContributor.class.isAssignableFrom(serviceContract)) { + if (metadataContributor != null) { + return (Collection) Arrays.asList(domainBinder, metadataContributor); + } + else { + return Collections.singletonList((S) domainBinder); + } + } + else { + return super.loadJavaServices(serviceContract); + } + } + }; + EventListenerIntegrator eventListenerIntegrator = new EventListenerIntegrator(hibernateEventListeners, eventListeners); + BootstrapServiceRegistry bootstrapServiceRegistry = createBootstrapServiceRegistryBuilder() + .applyIntegrator(eventListenerIntegrator) + .applyIntegrator(new MetadataIntegrator()) + .applyClassLoaderService(classLoaderService) + .build(); + StrategySelector strategySelector = bootstrapServiceRegistry.getService(StrategySelector.class); + + strategySelector.registerStrategyImplementor( + PropertyAccessStrategy.class, "traitProperty", TraitPropertyAccessStrategy.class + ); + + setSessionFactoryObserver(new SessionFactoryObserver() { + private static final long serialVersionUID = 1; + + public void sessionFactoryCreated(SessionFactory factory) {} + + public void sessionFactoryClosed(SessionFactory factory) { + if (serviceRegistry != null) { + ((ServiceRegistryImplementor) serviceRegistry).destroy(); + } + } + }); + + StandardServiceRegistryBuilder standardServiceRegistryBuilder = createStandardServiceRegistryBuilder(bootstrapServiceRegistry) + .applySettings(getProperties()); + + StandardServiceRegistry serviceRegistry = standardServiceRegistryBuilder.build(); + sessionFactory = super.buildSessionFactory(serviceRegistry); + this.serviceRegistry = serviceRegistry; + + return sessionFactory; + } + + /** + * Creates the {@link BootstrapServiceRegistryBuilder} to use + * + * @return The {@link BootstrapServiceRegistryBuilder} + */ + protected BootstrapServiceRegistryBuilder createBootstrapServiceRegistryBuilder() { + return new BootstrapServiceRegistryBuilder(); + } + + /** + * Creates the standard service registry builder. Subclasses can override to customize the creation of the StandardServiceRegistry + * + * @param bootstrapServiceRegistry The {@link BootstrapServiceRegistry} + * @return The {@link StandardServiceRegistryBuilder} + */ + protected StandardServiceRegistryBuilder createStandardServiceRegistryBuilder(BootstrapServiceRegistry bootstrapServiceRegistry) { + return new StandardServiceRegistryBuilder(bootstrapServiceRegistry); + } + + /** + * Default listeners. + * @param listeners the listeners + */ + public void setEventListeners(Map listeners) { + eventListeners = listeners; + } + + /** + * User-specifiable extra listeners. + * @param listeners the listeners + */ + public void setHibernateEventListeners(HibernateEventListeners listeners) { + hibernateEventListeners = listeners; + } + + public ServiceRegistry getServiceRegistry() { + return serviceRegistry; + } + + @Override + protected void reset() { + super.reset(); + try { + GrailsIdentifierGeneratorFactory.applyNewInstance(this); + } + catch (Exception e) { + // ignore exception + } + } + + public void setMetadataContributor(MetadataContributor metadataContributor) { + this.metadataContributor = metadataContributor; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernatePersistentEntity.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernatePersistentEntity.java new file mode 100644 index 00000000000..fad837c38f1 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernatePersistentEntity.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg; + +import org.grails.datastore.mapping.model.AbstractClassMapping; +import org.grails.datastore.mapping.model.AbstractPersistentEntity; +import org.grails.datastore.mapping.model.ClassMapping; +import org.grails.datastore.mapping.model.MappingContext; +import org.grails.datastore.mapping.model.PersistentEntity; + +/** + * Persistent entity implementation for Hibernate + * + * @author Graeme Rocher + * @since 5.0 + */ +public class HibernatePersistentEntity extends AbstractPersistentEntity { + private final AbstractClassMapping classMapping; + + public HibernatePersistentEntity(Class javaClass, final MappingContext context) { + super(javaClass, context); + + this.classMapping = new AbstractClassMapping<>(this, context) { + Mapping mappedForm = (Mapping) context.getMappingFactory().createMappedForm(HibernatePersistentEntity.this); + + @Override + public PersistentEntity getEntity() { + return HibernatePersistentEntity.this; + } + + @Override + public Mapping getMappedForm() { + return mappedForm; + } + }; + + } + + @Override + protected boolean includeIdentifiers() { + return true; + } + + @Override + public ClassMapping getMapping() { + return this.classMapping; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Identity.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Identity.groovy new file mode 100644 index 00000000000..2356838a4cc --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Identity.groovy @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +import org.springframework.beans.MutablePropertyValues +import org.springframework.validation.DataBinder + +import org.grails.datastore.mapping.config.Property + +/** + * Defines the identity generation strategy. In the case of a 'composite' identity the properties + * array defines the property names that formulate the composite id. + * + * @author Graeme Rocher + * @since 1.0 + */ +@CompileStatic +@Builder(builderStrategy = SimpleStrategy, prefix = '') +class Identity extends Property { + + /** + * The generator to use + */ + String generator = 'native' + /** + * The column to map to + */ + String column = 'id' + /** + * The name of the id property + */ + String name + /** + * The natural id definition + */ + NaturalId natural + /** + * The type + */ + Class type = Long + /** + * Any parameters (for example for the generator) + */ + Map params = [:] + + /** + * Define the natural id + * @param naturalIdDef The callable + * @return This id + */ + Identity naturalId(@DelegatesTo(NaturalId) Closure naturalIdDef) { + naturalIdDef.setDelegate(new NaturalId()) + naturalIdDef.setResolveStrategy(Closure.DELEGATE_ONLY) + naturalIdDef.call() + return this + } + + String toString() { "id[generator:$generator, column:$column, type:$type]" } + + /** + * Configures a new Identity instance + * + * @param config The configuration + * @return The new instance + */ + static Identity configureNew(@DelegatesTo(Identity) Closure config) { + Identity property = new Identity() + return configureExisting(property, config) + } + + /** + * Configures an existing Identity instance + * + * @param config The configuration + * @return The new instance + */ + static Identity configureExisting(Identity property, Map config) { + DataBinder dataBinder = new DataBinder(property) + dataBinder.bind(new MutablePropertyValues(config)) + return property + } + /** + * Configures an existing PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static Identity configureExisting(Identity property, @DelegatesTo(Identity) Closure config) { + config.setDelegate(property) + config.setResolveStrategy(Closure.DELEGATE_ONLY) + config.call() + return property + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/IdentityEnumType.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/IdentityEnumType.java new file mode 100644 index 00000000000..0b56d2016ea --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/IdentityEnumType.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.AbstractStandardBasicType; +import org.hibernate.type.spi.TypeConfiguration; +import org.hibernate.usertype.ParameterizedType; +import org.hibernate.usertype.UserType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Hibernate Usertype that enum values by their ID. + * + * @author Siegfried Puchbauer + * @author Graeme Rocher + * + * @since 1.1 + */ +public class IdentityEnumType implements UserType, ParameterizedType, Serializable { + + private static final long serialVersionUID = -6625622185856547501L; + + private static final Logger LOG = LoggerFactory.getLogger(IdentityEnumType.class); + + private static TypeConfiguration typeConfiguration = new TypeConfiguration(); + public static final String ENUM_ID_ACCESSOR = "getId"; + + public static final String PARAM_ENUM_CLASS = "enumClass"; + + private static final Map>, BidiEnumMap> ENUM_MAPPINGS = new HashMap<>(); + protected Class> enumClass; + protected BidiEnumMap bidiMap; + protected AbstractStandardBasicType type; + protected int[] sqlTypes; + + public static BidiEnumMap getBidiEnumMap(Class> cls) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { + BidiEnumMap m = ENUM_MAPPINGS.get(cls); + if (m == null) { + synchronized (ENUM_MAPPINGS) { + if (!ENUM_MAPPINGS.containsKey(cls)) { + m = new BidiEnumMap(cls); + ENUM_MAPPINGS.put(cls, m); + } + else { + m = ENUM_MAPPINGS.get(cls); + } + } + } + return m; + } + + @SuppressWarnings("unchecked") + public void setParameterValues(Properties properties) { + try { + enumClass = (Class>) Thread.currentThread().getContextClassLoader().loadClass( + (String) properties.get(PARAM_ENUM_CLASS)); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Building ID-mapping for Enum Class %s", enumClass.getName())); + } + bidiMap = getBidiEnumMap(enumClass); + type = (AbstractStandardBasicType) typeConfiguration.getBasicTypeRegistry().getRegisteredType(bidiMap.keyType.getName()); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Mapped Basic Type is %s", type)); + } + sqlTypes = type.sqlTypes(null); + } + catch (Exception e) { + throw new MappingException("Error mapping Enum Class using IdentifierEnumType", e); + } + } + + public int[] sqlTypes() { + return sqlTypes; + } + + public Class returnedClass() { + return enumClass; + } + + public boolean equals(Object o1, Object o2) throws HibernateException { + return o1 == o2; + } + + public int hashCode(Object o) throws HibernateException { + return o.hashCode(); + } + + @Override + public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { + Object id = type.nullSafeGet(rs, names[0], session); + if ((!rs.wasNull()) && id != null) { + return bidiMap.getEnumValue(id); + } + return null; + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { + if (value == null) { + st.setNull(index, sqlTypes[0]); + } + else { + type.nullSafeSet(st, bidiMap.getKey(value), index, session); + } + } + + public Object deepCopy(Object o) throws HibernateException { + return o; + } + + public boolean isMutable() { + return false; + } + + public Serializable disassemble(Object o) throws HibernateException { + return (Serializable) o; + } + + public Object assemble(Serializable cached, Object owner) throws HibernateException { + return cached; + } + + public Object replace(Object orig, Object target, Object owner) throws HibernateException { + return orig; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static class BidiEnumMap implements Serializable { + + private static final long serialVersionUID = 3325751131102095834L; + private final Map enumToKey; + private final Map keytoEnum; + private Class keyType; + + private BidiEnumMap(Class enumClass) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + if (LOG.isDebugEnabled()) { + LOG.debug("Building Bidirectional Enum Map..."); + } + + EnumMap enumToKey = new EnumMap(enumClass); + HashMap keytoEnum = new HashMap(); + + Method idAccessor = enumClass.getMethod(ENUM_ID_ACCESSOR); + + keyType = idAccessor.getReturnType(); + + Method valuesAccessor = enumClass.getMethod("values"); + Object[] values = (Object[]) valuesAccessor.invoke(enumClass); + + for (Object value : values) { + Object id = idAccessor.invoke(value); + enumToKey.put((Enum) value, id); + if (keytoEnum.containsKey(id)) { + LOG.warn(String.format("Duplicate Enum ID '%s' detected for Enum %s!", id, enumClass.getName())); + } + keytoEnum.put(id, value); + } + + this.enumToKey = Collections.unmodifiableMap(enumToKey); + this.keytoEnum = Collections.unmodifiableMap(keytoEnum); + } + + public Object getEnumValue(Object id) { + return keytoEnum.get(id); + } + + public Object getKey(Object enumValue) { + return enumToKey.get(enumValue); + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/InstanceProxy.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/InstanceProxy.groovy new file mode 100644 index 00000000000..bed30aed4b4 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/InstanceProxy.groovy @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileStatic + +import org.grails.orm.hibernate.AbstractHibernateGormInstanceApi +import org.grails.orm.hibernate.AbstractHibernateGormValidationApi + +@CompileStatic +class InstanceProxy { + + protected instance + protected AbstractHibernateGormValidationApi validateApi + protected AbstractHibernateGormInstanceApi instanceApi + + protected final Set validateMethods + + InstanceProxy(instance, AbstractHibernateGormInstanceApi instanceApi, AbstractHibernateGormValidationApi validateApi) { + this.instance = instance + this.instanceApi = instanceApi + this.validateApi = validateApi + validateMethods = validateApi.methods*.name as Set + validateMethods.remove('getValidator') + validateMethods.remove('setValidator') + validateMethods.remove('getBeforeValidateHelper') + validateMethods.remove('setBeforeValidateHelper') + validateMethods.remove('getValidateMethod') + validateMethods.remove('setValidateMethod') + } + + def invokeMethod(String name, args) { + if (validateMethods.contains(name)) { + validateApi.invokeMethod(name, prependToArray(instance, (Object[]) args)) + } + else { + instanceApi.invokeMethod(name, prependToArray(instance, (Object[]) args)) + } + } + + private final static Object[] prependToArray(Object item, Object[] array) { + def list = new ArrayList(array.length + 1) + list.add(item) + list.addAll(array) + list as Object[] + } + + void setProperty(String name, val) { + instanceApi.setProperty(name, val) + } + + def getProperty(String name) { + instanceApi.getProperty(name) + } + + void putAt(String name, val) { + instanceApi.setProperty(name, val) + } + + def getAt(String name) { + instanceApi.getProperty(name) + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/JoinTable.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/JoinTable.groovy new file mode 100644 index 00000000000..c3bc008128f --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/JoinTable.groovy @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.AutoClone +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +/** + * Represents a Join table in Grails mapping. It has a name which represents the name of the table, a key + * for the primary key and a column which is the other side of the join. + * + * @author Graeme Rocher + * @since 1.0 + */ +@AutoClone +@Builder(builderStrategy = SimpleStrategy, prefix = '') +@CompileStatic +class JoinTable extends Table { + + /** + * The foreign key column + */ + ColumnConfig key + /** + * The child id column + */ + ColumnConfig column + + /** + * Configures the column + * @param columnConfig The column config + * @return This join table config + */ + JoinTable key(@DelegatesTo(ColumnConfig) Closure columnConfig) { + key = ColumnConfig.configureNew(columnConfig) + return this + } + /** + * Configures the column + * @param columnConfig The column config + * @return This join table config + */ + JoinTable column(@DelegatesTo(ColumnConfig) Closure columnConfig) { + column = ColumnConfig.configureNew(columnConfig) + return this + } + + /** + * Configures the column + * @param columnName the column name + * @return This join table config + */ + JoinTable key(String columnName) { + key = new ColumnConfig(name: columnName) + return this + } + + /** + * Configures the column + * @param columnName the column name + * @return This join table config + */ + JoinTable column(String columnName) { + column = new ColumnConfig(name: columnName) + return this + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Mapping.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Mapping.groovy new file mode 100644 index 00000000000..c45f11d9cda --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Mapping.groovy @@ -0,0 +1,589 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +import org.springframework.beans.MutablePropertyValues +import org.springframework.validation.DataBinder + +import org.grails.datastore.mapping.config.Entity +import org.grails.datastore.mapping.config.Property +import org.grails.datastore.mapping.model.config.GormProperties + +/** + * Models the mapping from GORM classes to the db. + * + * @author Graeme Rocher + * @since 1.0 + */ +@CompileStatic +@Builder(builderStrategy = SimpleStrategy, prefix = '') +class Mapping extends Entity { + + /** + * Custom hibernate user types + */ + Map userTypes = [:] + + /** + * Return a type name of the known custom user types + */ + String getTypeName(Class theClass) { + def type = userTypes[theClass] + if (type == null) { + return null + } + + return type instanceof Class ? ((Class) type).name : type.toString() + } + + /** + * The table + */ + Table table = new Table() + + /** + * The table name + */ + String getTableName() { table?.name } + + /** + * Set the table name + */ + void setTableName(String name) { table?.name = name } + + /** + * Whether the class is versioned for optimistic locking + */ + boolean versioned = true + + /** + * Sets whether to use table-per-hierarchy or table-per-subclass mapping + */ + boolean tablePerHierarchy = true + + /** + * Sets whether to use table-per-concrete-class or table-per-subclass mapping + */ + boolean tablePerConcreteClass = false + + /** + * Sets whether packaged domain classes should be auto-imported in HQL queries + */ + boolean autoImport = true + + /** + * The configuration for each property + */ + Map columns = [:] + + /** + * The identity definition + */ + Property identity = new Identity() + + /** + * Caching config + */ + CacheConfig cache + + /** + * Used to hold the names and directions of the default property to sort by + */ + SortConfig sort = new SortConfig() + + /** + * Value used to discriminate entities in table-per-hierarchy inheritance mapping + */ + DiscriminatorConfig discriminator + + /** + * Obtains a PropertyConfig object for the given name + */ + @Override + PropertyConfig getPropertyConfig(String name) { columns[name] } + + /** + * The batch size to use for lazy loading + */ + Integer batchSize + + /** + * Whether to use dynamically created update queries, at the cost of some performance + */ + boolean dynamicUpdate = false + + /** + * Whether to use dynamically created insert queries, at the cost of some performance + */ + boolean dynamicInsert = false + + /** + * DDL comment. + */ + String comment + + boolean isTablePerConcreteClass() { + return tablePerConcreteClass + } + + void setTablePerConcreteClass(boolean tablePerConcreteClass) { + this.tablePerHierarchy = !tablePerConcreteClass + this.tablePerConcreteClass = tablePerConcreteClass + } + + @Override + Map getPropertyConfigs() { + return columns + } + /** + * Define the table name + * @param name The table name + * @return This mapping + */ + Mapping table(String name) { + this.table.name = name + return this + } + + /** + * Define the table config + * + * @param tableConfig The table config + * @return This mapping + */ + Mapping table(@DelegatesTo(Table) Closure tableConfig) { + Table.configureExisting(table, tableConfig) + return this + } + + /** + * Define the table config + * + * @param tableConfig The table config + * @return This mapping + */ + Mapping table(Map tableConfig) { + Table.configureExisting(table, tableConfig) + return this + } + /** + * Define the identity config + * @param identityConfig The id config + * @return This mapping + */ + @Override + Mapping id(Map identityConfig) { + if (identity instanceof Identity) { + Identity.configureExisting((Identity) identity, identityConfig) + } + return this + } + /** + * Define the identity config + * @param identityConfig The id config + * @return This mapping + */ + @Override + Mapping id(@DelegatesTo(Identity) Closure identityConfig) { + if (identity instanceof Identity) { + Identity.configureExisting((Identity) identity, identityConfig) + } + return this + } + + /** + * Define the identity config + * @param identityConfig The id config + * @return This mapping + */ + + Mapping id(CompositeIdentity compositeIdentity) { + this.identity = compositeIdentity + return this + } + + /** + * Define the cache config + * @param cacheConfig The cache config + * @return This mapping + */ + Mapping cache(@DelegatesTo(CacheConfig) Closure cacheConfig) { + if (this.cache == null) { + this.cache = new CacheConfig() + } + CacheConfig.configureExisting(cache, cacheConfig) + return this + } + + /** + * Define the cache config + * @param cacheConfig The cache config + * @return This mapping + */ + Mapping cache(Map cacheConfig) { + if (this.cache == null) { + this.cache = new CacheConfig() + } + CacheConfig.configureExisting(cache, cacheConfig) + return this + } + + /** + * Define the cache config + * @param cacheConfig The cache config + * @return This mapping + */ + Mapping cache(String usage) { + if (this.cache == null) { + this.cache = new CacheConfig() + } + this.cache.usage = usage + this.cache.enabled = true + return this + } + + /** + * Configures sorting + * @param name The name + * @param direction The direction + * @return This mapping + */ + Mapping sort(String name, String direction) { + if (name && direction) { + this.sort.name = name + this.sort.direction = direction + } + return this + } + + /** + * Configures sorting + * @param name The name + * @param direction The direction + * @return This mapping + */ + Mapping sort(Map nameAndDirections) { + if (nameAndDirections) { + this.sort.namesAndDirections = nameAndDirections + } + return this + } + + /** + * Configures the discriminator + * @param discriminatorDef The discriminator + * @return This mapping + */ + Mapping discriminator(@DelegatesTo(DiscriminatorConfig) Closure discriminatorDef) { + if (discriminator == null) { + discriminator = new DiscriminatorConfig() + } + discriminatorDef.setDelegate(discriminator) + discriminatorDef.setResolveStrategy(Closure.DELEGATE_ONLY) + discriminatorDef.call() + return this + } + + /** + * Configures the discriminator + * @param the discriminator value + * @return This mapping + */ + Mapping discriminator(String value) { + if (discriminator == null) { + discriminator = new DiscriminatorConfig() + } + discriminator.value = value + return this + } + + /** + * Configures the discriminator + * @param discriminatorDef The discriminator + * @return This mapping + */ + Mapping discriminator(Map args) { + if (args != null) { + if (discriminator == null) { + discriminator = new DiscriminatorConfig() + } + + String value = args.remove('value')?.toString() + discriminator.value = value + if (args.column instanceof String) { + discriminator.column = new ColumnConfig(name: args.column.toString()) + } + else if (args.column instanceof Map) { + ColumnConfig config = new ColumnConfig() + DataBinder dataBinder = new DataBinder(config) + dataBinder.bind(new MutablePropertyValues((Map) args.column)) + discriminator.column = config + } + discriminator.type(args.remove('type')) + if (args.containsKey('insert')) { + discriminator.insertable(args.remove('insert') as Boolean) + } + if (args.containsKey('insertable')) { + discriminator.insertable(args.remove('insertable') as Boolean) + } + discriminator.formula(args.remove('formula')?.toString()) + } + return this + } + + /** + * Define a new composite id + * @param propertyNames + * @return + */ + CompositeIdentity composite(String...propertyNames) { + identity = new CompositeIdentity(propertyNames: propertyNames) + return (CompositeIdentity) identity + } + + /** + *

Configures whether to use versioning for optimistic locking + * { version false } + * + * @param isVersioned True if a version property should be configured + */ + @CompileStatic + Mapping version(boolean isVersioned) { + versioned = isVersioned + return this + } + + /** + *

Configures the name of the version column + * { version 'foo' } + * + * @param isVersioned True if a version property should be configured + */ + @CompileStatic + @Override + Mapping version(Map versionConfig) { + PropertyConfig pc = getOrInitializePropertyConfig(GormProperties.VERSION) + PropertyConfig.configureExisting(pc, versionConfig) + return this + } + + /** + *

Configures the name of the version column + * { version 'foo' } + * + * @param isVersioned True if a version property should be configured + */ + @CompileStatic + Mapping version(String versionColumn) { + PropertyConfig pc = getOrInitializePropertyConfig(GormProperties.VERSION) + pc.columns << new ColumnConfig(name: versionColumn) + return this + } + + /** + * Configure a property + * @param name The name of the property + * @param propertyConfig The property config + * @return This mapping + */ + @Override + Mapping property(String name, @DelegatesTo(PropertyConfig) Closure propertyConfig) { + PropertyConfig pc = getOrInitializePropertyConfig(name) + PropertyConfig.configureExisting(pc, propertyConfig) + return this + } + + /** + * Configure a property + * @param name The name of the property + * @param propertyConfig The property config + * @return This mapping + */ + @Override + Mapping property(String name, Map propertyConfig) { + PropertyConfig pc = getOrInitializePropertyConfig(name) + PropertyConfig.configureExisting(pc, propertyConfig) + return this + } + + /** + * Configure a new property + * @param name The name of the property + * @param propertyConfig The property config + * @return This mapping + */ + @Override + PropertyConfig property(@DelegatesTo(PropertyConfig) Closure propertyConfig) { + if (columns.containsKey('*')) { + PropertyConfig cloned = cloneGlobalConstraint() + return PropertyConfig.configureExisting(cloned, propertyConfig) + } + else { + return PropertyConfig.configureNew(propertyConfig) + } + } + + /** + * Configure the version + * + * @param versionConfig The version config + * @return This entity + */ + @Override + Entity version(@DelegatesTo(PropertyConfig) Closure versionConfig) { + return super.version(versionConfig) + } + /** + * Configure a new property + * @param name The name of the property + * @param propertyConfig The property config + * @return This mapping + */ + @Override + PropertyConfig property(Map propertyConfig) { + if (columns.containsKey('*')) { + // apply global constraints constraints + PropertyConfig cloned = cloneGlobalConstraint() + return PropertyConfig.configureExisting(cloned, propertyConfig) + } + else { + return PropertyConfig.configureNew(propertyConfig) + } + } + + /** + * Configures a new Mapping instance + * + * @param config The configuration + * @return The new instance + */ + static Mapping configureNew(@DelegatesTo(Mapping) Closure config) { + Mapping property = new Mapping() + return configureExisting(property, config) + } + + /** + * Configures an existing Mapping instance + * + * @param config The configuration + * @return The new instance + */ + static Mapping configureExisting(Mapping mapping, Map config) { + DataBinder dataBinder = new DataBinder(mapping) + dataBinder.bind(new MutablePropertyValues(config)) + + return mapping + } + + /** + * Configures an existing Mapping instance + * + * @param config The configuration + * @return The new instance + */ + static Mapping configureExisting(Mapping mapping, @DelegatesTo(Mapping) Closure config) { + config.setDelegate(mapping) + config.setResolveStrategy(Closure.DELEGATE_ONLY) + config.call() + return mapping + } + + @Override + def propertyMissing(String name, Object val) { + if (val instanceof Closure) { + property(name, (Closure) val) + } + else if (val instanceof PropertyConfig) { + columns[name] = ((PropertyConfig) val) + } + else { + throw new MissingPropertyException(name, Mapping) + } + } + + @CompileDynamic + @Override + def methodMissing(String name, Object args) { + if (args && args.getClass().isArray()) { + if (args[0] instanceof Closure) { + property(name, (Closure) args[0]) + } + else if (args[0] instanceof PropertyConfig) { + columns[name] = (PropertyConfig) args[0] + } + else if (args[0] instanceof Map) { + PropertyConfig property = getOrInitializePropertyConfig(name) + Map namedArgs = (Map) args[0] + if (args[-1] instanceof Closure) { + PropertyConfig.configureExisting( + property, + ((Closure) args[-1]) + ) + + } + PropertyConfig.configureExisting(property, namedArgs) + } + else { + throw new MissingMethodException(name, getClass(), args) + } + } + else { + throw new MissingMethodException(name, getClass(), args) + } + } + + @Override + protected PropertyConfig getOrInitializePropertyConfig(String name) { + PropertyConfig pc = columns[name] + if (pc == null && columns.containsKey('*')) { + // apply global constraints constraints + PropertyConfig globalConstraints = columns.get('*') + if (globalConstraints != null) { + pc = (PropertyConfig) globalConstraints.clone() + if (pc.columns.size() == 1) { + pc.firstColumnIsColumnCopy = true + } + } + } + else { + pc = columns[name] + } + if (pc == null) { + pc = new PropertyConfig() + columns[name] = pc + } + return pc + } + + @Override + protected PropertyConfig cloneGlobalConstraint() { + // apply global constraints constraints + PropertyConfig globalConstraints = columns.get('*') + PropertyConfig cloned = (PropertyConfig) globalConstraints.clone() + if (cloned.columns.size() == 1) { + cloned.firstColumnIsColumnCopy = true + } + cloned + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/NaturalId.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/NaturalId.groovy new file mode 100644 index 00000000000..ff0d6f25d4e --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/NaturalId.groovy @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +/** + * @author Graeme Rocher + * @since 1.1 + */ +@CompileStatic +@Builder(builderStrategy = SimpleStrategy, prefix = '') +class NaturalId { + + /** + * The property names that make up the natural id + */ + List propertyNames = [] + /** + * Whether the natural id is mutable + */ + boolean mutable = false +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PersistentEntityNamingStrategy.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PersistentEntityNamingStrategy.java new file mode 100644 index 00000000000..3149415a47a --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PersistentEntityNamingStrategy.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.cfg; + +import org.grails.datastore.mapping.model.PersistentEntity; + +/** + * Allows plugging into to custom naming strategies + * + * @author Graeme Rocher + * @since 5.0 + */ +public interface PersistentEntityNamingStrategy { + + String resolveTableName(PersistentEntity entity); + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyConfig.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyConfig.groovy new file mode 100644 index 00000000000..850a3058048 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyConfig.groovy @@ -0,0 +1,476 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +import jakarta.persistence.FetchType + +import org.hibernate.FetchMode + +import org.springframework.beans.MutablePropertyValues +import org.springframework.validation.DataBinder + +import org.grails.datastore.mapping.config.Property + +/** + * Custom mapping for a single domain property. Note that a property + * can have multiple columns via a component or a user type. + * + * @since 1.0.4 + * @author pledbrook + */ +@CompileStatic +@Builder(builderStrategy = SimpleStrategy, prefix = '') +class PropertyConfig extends Property { + + PropertyConfig() { + setFetchStrategy(null) + } + + @PackageScope + // Whether the first column is created from cloning this instance + boolean firstColumnIsColumnCopy = false + + boolean explicitSaveUpdateCascade + + /** + * The Hibernate type or user type of the property. This can be + * a string or a class. + */ + def type + + /** + * The parameters for the property that can be used to + * configure a Hibernate ParameterizedType implementation. + */ + Properties typeParams + + /** + * The default sort property name + */ + String sort + + /** + * The default sort order + */ + String order + + /** + * The batch size used for lazy loading + */ + Integer batchSize + + /** + * Whether to ignore ObjectNotFoundException + */ + boolean ignoreNotFound = false + + /** + * Whether or not this is column is insertable by hibernate + */ + boolean insertable = true + + /** + * Whether or not this column is updatable by hibernate + */ + boolean updatable = true + + /** + * Whether or not this column is updatable by hibernate + * + * @deprecated Use updatable instead + */ + @Deprecated // Cheap to keep around for backwards compatibility + boolean getUpdateable() { + return updatable + } + + /** + * Whether or not this column is updatable by hibernate + * @deprecated Use updatable instead + */ + @Deprecated // Cheap to keep around for backwards compatibility + void setUpdateable(boolean updateable) { + this.updatable = updateable + } + + /** + * The columns + */ + List columns = [] + + /** + * Configure a column + * @param columnDef The column definition + * @return This property config + */ + PropertyConfig column(@DelegatesTo(ColumnConfig) Closure columnDef) { + if (columns.size() == 1 && firstColumnIsColumnCopy) { + firstColumnIsColumnCopy = false + ColumnConfig.configureExisting(columns[0], columnDef) + } + else { + columns.add(ColumnConfig.configureNew(columnDef)) + } + return this + } + + /** + * Configure a column + * @param columnDef The column definition + * @return This property config + */ + PropertyConfig column(Map columnDef) { + if (columns.size() == 1 && firstColumnIsColumnCopy) { + firstColumnIsColumnCopy = false + ColumnConfig.configureExisting(columns[0], columnDef) + } + else { + columns.add(ColumnConfig.configureNew(columnDef)) + } + return this + } + + /** + * Configure a column + * @param columnDef The column definition + * @return This property config + */ + PropertyConfig column(String columnDef) { + if (columns.size() == 1 && firstColumnIsColumnCopy) { + firstColumnIsColumnCopy = false + columns[0].name = columnDef + } + else { + columns.add(ColumnConfig.configureNew(name: columnDef)) + } + return this + } + /** + * The cache configuration + */ + CacheConfig cache + + /** + * Define the cache config + * @param cacheConfig The cache config + * @return This mapping + */ + PropertyConfig cache(@DelegatesTo(CacheConfig) Closure cacheConfig) { + if (this.cache == null) { + this.cache = new CacheConfig() + } + CacheConfig.configureExisting(cache, cacheConfig) + return this + } + + /** + * Define the cache config + * @param cacheConfig The cache config + * @return This mapping + */ + PropertyConfig cache(Map cacheConfig) { + if (this.cache == null) { + this.cache = new CacheConfig() + } + CacheConfig.configureExisting(cache, cacheConfig) + return this + } + + /** + * The join table configuration + */ + JoinTable joinTable = new JoinTable() + + /** + * The join table configuration + */ + PropertyConfig joinTable(@DelegatesTo(JoinTable) Closure joinTableDef) { + JoinTable.configureExisting(joinTable, joinTableDef) + return this + } + + /** + * The join table configuration + */ + PropertyConfig joinTable(String tableName) { + joinTable.name = tableName + return this + } + + @Override + void setUnique(boolean unique) { + super.setUnique(unique) + if (columns.size() == 1) { + columns[0].unique = unique + } + } + /** + * The join table configuration + */ + PropertyConfig joinTable(Map joinTableDef) { + DataBinder dataBinder = new DataBinder(joinTable) + dataBinder.bind(new MutablePropertyValues(joinTableDef)) + if (joinTableDef.key) { + joinTable.key(joinTableDef.key.toString()) + } + if (joinTableDef.column) { + joinTable.column(joinTableDef.column.toString()) + } + return this + } + + /** + * @param fetch The Hibernate {@link FetchMode} + */ + void setFetch(FetchMode fetch) { + if (FetchMode.JOIN.equals(fetch)) { + super.setFetchStrategy(FetchType.EAGER) + } + else { + super.setFetchStrategy(FetchType.LAZY) + } + } + + /** + * @return The Hibernate {@link FetchMode} + */ + FetchMode getFetchMode() { + FetchType strategy = super.getFetchStrategy() + if (strategy == null) { + return FetchMode.DEFAULT + } + switch (strategy) { + case FetchType.EAGER: + return FetchMode.JOIN + case FetchType.LAZY: + return FetchMode.SELECT + default: + return FetchMode.DEFAULT + } + } + /** + * The column used to produce the index for index based collections (lists and maps) + */ + PropertyConfig indexColumn + + /** + * The column used to produce the index for index based collections (lists and maps) + */ + PropertyConfig indexColumn(@DelegatesTo(PropertyConfig) Closure indexColumnConfig) { + this.indexColumn = configureNew(indexColumnConfig) + return this + } + + /** + * Configures a new PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static PropertyConfig configureNew(@DelegatesTo(PropertyConfig) Closure config) { + PropertyConfig property = new PropertyConfig() + return configureExisting(property, config) + } + + /** + * Configures a new PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static PropertyConfig configureNew(Map config) { + PropertyConfig property = new PropertyConfig() + return configureExisting(property, config) + } + + /** + * Configures an existing PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static PropertyConfig configureExisting(PropertyConfig property, Map config) { + DataBinder dataBinder = new DataBinder(property) + dataBinder.bind(new MutablePropertyValues(config)) + + ColumnConfig cc + if (property.columns) { + cc = property.columns[0] + } + else { + cc = new ColumnConfig() + property.columns.add(cc) + } + if (config.column) { + config.name = config.column + } + ColumnConfig.configureExisting(cc, config) + + return property + } + /** + * Configures an existing PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static PropertyConfig configureExisting(PropertyConfig property, @DelegatesTo(PropertyConfig) Closure config) { + config.setDelegate(property) + config.setResolveStrategy(Closure.DELEGATE_ONLY) + config.call() + return property + } + + /** + * Shortcut to get the column name for this property. + * @throws RuntimeException if this property maps to more than one + * column. + */ + String getColumn() { + checkHasSingleColumn() + if (columns.isEmpty()) return null + return columns[0].name + } + + String getEnumType() { + checkHasSingleColumn() + if (columns.isEmpty()) return 'default' + return columns[0].enumType + } + + /** + * Shortcut to get the SQL type of the corresponding column. + * @throws RuntimeException if this property maps to more than one + * column. + */ + String getSqlType() { + checkHasSingleColumn() + if (columns.isEmpty()) return null + return columns[0].sqlType + } + + /** + * Shortcut to get the index setting for this property's column. + * @throws RuntimeException if this property maps to more than one + * column. + */ + String getIndexName() { + checkHasSingleColumn() + if (columns.isEmpty()) return null + return columns[0].index?.toString() + } + + /** + * Shortcut to determine whether the property's column is configured + * to be unique. + * @throws RuntimeException if this property maps to more than one + * column. + */ + boolean isUnique() { + if (columns.size() > 1) { + return super.isUnique() + } + else { + if (columns.isEmpty()) return super.isUnique() + return columns[0].unique + } + } + + /** + * Shortcut to get the length of this property's column. + * @throws RuntimeException if this property maps to more than one + * column. + */ + int getLength() { + checkHasSingleColumn() + if (columns.isEmpty()) return -1 + return columns[0].length + } + + /** + * Shortcut to get the precision of this property's column. + * @throws RuntimeException if this property maps to more than one + * column. + */ + int getPrecision() { + checkHasSingleColumn() + if (columns.isEmpty()) return -1 + return columns[0].precision + } + + /** + * Shortcut to get the scale of this property's column. + * @throws RuntimeException if this property maps to more than one + * column. + */ + int getScale() { + checkHasSingleColumn() + if (columns.isEmpty()) { + return super.getScale() + } + return columns[0].scale + } + + @Override + void setScale(int scale) { + checkHasSingleColumn() + if (!columns.isEmpty()) { + columns[0].scale = scale + } + else { + super.setScale(scale) + } + } + + String toString() { + // TODO(Grails 8): updateable -> updatable + "property[type:$type, lazy:$lazy, columns:$columns, insertable:${insertable}, updateable:${updatable}]" + } + + protected void checkHasSingleColumn() { + if (columns?.size() > 1) { + throw new RuntimeException('Cannot treat multi-column property as a single-column property') + } + } + + @Override + PropertyConfig clone() throws CloneNotSupportedException { + PropertyConfig pc = (PropertyConfig) super.clone() + + pc.fetch = fetchMode + pc.indexColumn = indexColumn != null ? (PropertyConfig) indexColumn.clone() : null + pc.cache = cache != null ? cache.clone() : cache + pc.joinTable = joinTable.clone() + if (typeParams != null) { + pc.typeParams = new Properties(typeParams) + } + + List newColumns = new ArrayList(columns.size()) + pc.columns = newColumns + for (c in columns) { + newColumns.add(c.clone()) + } + return pc + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy new file mode 100644 index 00000000000..0199bb608c7 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileStatic + +import org.grails.datastore.mapping.model.DatastoreConfigurationException + +/** + * Builder delegate that handles multiple-column definitions for a + * single domain property, e.g. + *

+ *   amount type: MonetaryAmountUserType, {
+ *       column name: "value"
+ *       column name: "currency_code", sqlType: "text"
+ *   }
+ * 
+ * + */ +@CompileStatic +class PropertyDefinitionDelegate { + + PropertyConfig config + + private int index = 0 + + PropertyDefinitionDelegate(PropertyConfig config) { + this.config = config + } + + ColumnConfig column(Map args) { + // Check that this column has a name + if (!args['name']) { + throw new DatastoreConfigurationException('Column definition must have a name!') + } + + // Create a new column configuration based on the mapping for this column. + ColumnConfig column + if (index < config.columns.size()) { + // configure existing + column = config.columns[0] + } + else { + column = new ColumnConfig() + // Append the new column configuration to the property config. + config.columns << column + } + column.name = args['name'] + column.sqlType = args['sqlType'] + column.enumType = args['enumType'] ?: column.enumType + column.index = args['index'] + column.unique = args['unique'] ?: false + column.length = args['length'] ? args['length'] as Integer : -1 + column.precision = args['precision'] ? args['precision'] as Integer : -1 + column.scale = args['scale'] ? args['scale'] as Integer : -1 + + index++ + return column + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Settings.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Settings.java new file mode 100644 index 00000000000..cd9d5e064ba --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Settings.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.cfg; + +/** + * Settings for Hibernate + * + * @author Graeme Rocher + * @since 6.0 + */ +public interface Settings extends org.grails.datastore.mapping.config.Settings { + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/SortConfig.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/SortConfig.groovy new file mode 100644 index 00000000000..37438d605da --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/SortConfig.groovy @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +/** + * Configures sorting + */ +@CompileStatic +@Builder(builderStrategy = SimpleStrategy, prefix = '') +class SortConfig { + + /** + * The property to sort bu + */ + String name + /** + * The direction to sort by + */ + String direction + + Map namesAndDirections + + Map getNamesAndDirections() { + if (namesAndDirections) { + return namesAndDirections + } + if (name) { + return [(name): direction] + } + Collections.emptyMap() + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Table.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Table.groovy new file mode 100644 index 00000000000..a7f697d44a3 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Table.groovy @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +import org.springframework.beans.MutablePropertyValues +import org.springframework.validation.DataBinder + +/** + * Represents a table definition in GORM. + * + * @author Graeme Rocher + * @since 1.1 + */ +@Builder(builderStrategy = SimpleStrategy, prefix = '') +@CompileStatic +class Table { + + /** + * The table name + */ + String name + /** + * The table catalog + */ + String catalog + /** + * The table schema + */ + String schema + + /** + * Configures a new Table instance + * + * @param config The configuration + * @return The new instance + */ + static Table configureNew(@DelegatesTo(Table) Closure config) { + Table table = new Table() + return configureExisting(table, config) + } + + /** + * Configures an existing Table instance + * + * @param config The configuration + * @return The new instance + */ + static Table configureExisting(Table table, Map config) { + DataBinder dataBinder = new DataBinder(table) + dataBinder.bind(new MutablePropertyValues(config)) + return table + } + /** + * Configures an existing PropertyConfig instance + * + * @param config The configuration + * @return The new instance + */ + static Table configureExisting(Table table, @DelegatesTo(Table) Closure config) { + config.setDelegate(table) + config.setResolveStrategy(Closure.DELEGATE_ONLY) + config.call() + return table + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/compiler/HibernateEntityTransformation.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/compiler/HibernateEntityTransformation.groovy new file mode 100644 index 00000000000..ea09484403b --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/compiler/HibernateEntityTransformation.groovy @@ -0,0 +1,347 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.compiler + +import java.lang.reflect.Modifier + +import groovy.transform.CompilationUnitAware +import groovy.transform.CompileStatic +import org.apache.groovy.ast.tools.AnnotatedNodeUtils +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.AnnotatedNode +import org.codehaus.groovy.ast.AnnotationNode +import org.codehaus.groovy.ast.ClassCodeVisitorSupport +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.InnerClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.Parameter +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.ast.stmt.IfStatement +import org.codehaus.groovy.ast.stmt.ReturnStatement +import org.codehaus.groovy.ast.stmt.Statement +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.transform.ASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.codehaus.groovy.transform.TransformWithPriority +import org.codehaus.groovy.transform.sc.StaticCompilationVisitor + +import jakarta.persistence.Transient + +import org.hibernate.engine.spi.EntityEntry +import org.hibernate.engine.spi.ManagedEntity +import org.hibernate.engine.spi.PersistentAttributeInterceptable +import org.hibernate.engine.spi.PersistentAttributeInterceptor + +import grails.gorm.dirty.checking.DirtyCheckedProperty +import org.apache.grails.common.compiler.GroovyTransformOrder +import org.grails.compiler.gorm.GormEntityTransformation +import org.grails.datastore.mapping.model.config.GormProperties +import org.grails.datastore.mapping.reflect.AstUtils +import org.grails.datastore.mapping.reflect.NameUtils + +import static org.codehaus.groovy.ast.tools.GeneralUtils.args +import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS +import static org.codehaus.groovy.ast.tools.GeneralUtils.callX +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX +import static org.codehaus.groovy.ast.tools.GeneralUtils.equalsNullX +import static org.codehaus.groovy.ast.tools.GeneralUtils.fieldX +import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS +import static org.codehaus.groovy.ast.tools.GeneralUtils.neX +import static org.codehaus.groovy.ast.tools.GeneralUtils.param +import static org.codehaus.groovy.ast.tools.GeneralUtils.params +import static org.codehaus.groovy.ast.tools.GeneralUtils.propX +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS +import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX + +/** + * A transformation that transforms entities that implement the {@link grails.gorm.hibernate.annotation.ManagedEntity} trait, + * adding logic that intercepts getter and setter access to eliminate the need for proxies. + * + * @author Graeme Rocher + * @since 6.1 + */ +@CompileStatic +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +class HibernateEntityTransformation implements ASTTransformation, CompilationUnitAware, TransformWithPriority { + + private static final ClassNode MY_TYPE = new ClassNode(grails.gorm.hibernate.annotation.ManagedEntity) + private static final Object APPLIED_MARKER = new Object() + +// final boolean available = ClassUtils.isPresent("org.hibernate.SessionFactory") && Boolean.valueOf(System.getProperty("hibernate.enhance", "true")) + CompilationUnit compilationUnit + + @Override + void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { + AnnotatedNode parent = (AnnotatedNode) astNodes[1] + AnnotationNode node = (AnnotationNode) astNodes[0] + + if (!(astNodes[0] instanceof AnnotationNode) || !(astNodes[1] instanceof AnnotatedNode)) { + throw new RuntimeException("Internal error: wrong types: ${node.getClass()} / ${parent.getClass()}") + } + + if (!MY_TYPE.equals(node.getClassNode()) || !(parent instanceof ClassNode)) { + return + } + + ClassNode cNode = (ClassNode) parent + + visit(cNode, sourceUnit) + } + + void visit(ClassNode classNode, SourceUnit sourceUnit) { + if (classNode.getNodeMetaData(AstUtils.TRANSFORM_APPLIED_MARKER) == APPLIED_MARKER) { + return + } + + if ((classNode instanceof InnerClassNode) || classNode.isEnum()) { + // do not apply transform to enums or inner classes + return + } + + def mapWith = AstUtils.getPropertyFromHierarchy(classNode, GormProperties.MAPPING_STRATEGY) + String mapWithValue = mapWith?.initialExpression?.text + + if (mapWithValue != null && (mapWithValue != ('hibernate') || mapWithValue != GormProperties.DEFAULT_MAPPING_STRATEGY)) { + return + } + + new GormEntityTransformation(compilationUnit: compilationUnit).visit(classNode, sourceUnit) + + ClassNode managedEntityClassNode = ClassHelper.make(ManagedEntity) + ClassNode attributeInterceptableClassNode = ClassHelper.make(PersistentAttributeInterceptable) + ClassNode entityEntryClassNode = ClassHelper.make(EntityEntry) + ClassNode persistentAttributeInterceptorClassNode = ClassHelper.make(PersistentAttributeInterceptor) + + classNode.addInterface(managedEntityClassNode) + classNode.addInterface(attributeInterceptableClassNode) + String interceptorFieldName = '$$_hibernate_attributeInterceptor' + String entryHolderFieldName = '$$_hibernate_entityEntryHolder' + String previousManagedEntityFieldName = '$$_hibernate_previousManagedEntity' + String nextManagedEntityFieldName = '$$_hibernate_nextManagedEntity' + + def staticCompilationVisitor = new StaticCompilationVisitor(sourceUnit, classNode) + + AnnotationNode transientAnnotationNode = new AnnotationNode(ClassHelper.make(Transient)) + FieldNode entityEntryHolderField = classNode.addField(entryHolderFieldName, Modifier.PRIVATE | Modifier.TRANSIENT, entityEntryClassNode, null) + entityEntryHolderField + .addAnnotation(transientAnnotationNode) + + FieldNode previousManagedEntityField = classNode.addField(previousManagedEntityFieldName, Modifier.PRIVATE | Modifier.TRANSIENT, managedEntityClassNode, null) + previousManagedEntityField + .addAnnotation(transientAnnotationNode) + + FieldNode nextManagedEntityField = classNode.addField(nextManagedEntityFieldName, Modifier.PRIVATE | Modifier.TRANSIENT, managedEntityClassNode, null) + nextManagedEntityField + .addAnnotation(transientAnnotationNode) + + FieldNode interceptorField = classNode.addField(interceptorFieldName, Modifier.PRIVATE | Modifier.TRANSIENT, persistentAttributeInterceptorClassNode, null) + interceptorField + .addAnnotation(transientAnnotationNode) + + // add method: PersistentAttributeInterceptor $$_hibernate_getInterceptor() + def getInterceptorMethod = new MethodNode( + '$$_hibernate_getInterceptor', + Modifier.PUBLIC, + persistentAttributeInterceptorClassNode, + AstUtils.ZERO_PARAMETERS, + null, + returnS(varX(interceptorField)) + ) + classNode.addMethod(getInterceptorMethod) + AnnotatedNodeUtils.markAsGenerated(classNode, getInterceptorMethod) + staticCompilationVisitor.visitMethod(getInterceptorMethod) + + // add method: void $$_hibernate_setInterceptor(PersistentAttributeInterceptor interceptor) + def p1 = param(persistentAttributeInterceptorClassNode, 'interceptor') + def setInterceptorMethod = new MethodNode( + '$$_hibernate_setInterceptor', + Modifier.PUBLIC, + ClassHelper.VOID_TYPE, + params(p1), + null, + assignS(varX(interceptorField), varX(p1)) + ) + classNode.addMethod(setInterceptorMethod) + AnnotatedNodeUtils.markAsGenerated(classNode, setInterceptorMethod) + staticCompilationVisitor.visitMethod(setInterceptorMethod) + + // add method: Object $$_hibernate_getEntityInstance() + def getEntityInstanceMethod = new MethodNode( + '$$_hibernate_getEntityInstance', + Modifier.PUBLIC, + ClassHelper.OBJECT_TYPE, + AstUtils.ZERO_PARAMETERS, + null, + returnS(varX('this')) + ) + classNode.addMethod(getEntityInstanceMethod) + AnnotatedNodeUtils.markAsGenerated(classNode, getEntityInstanceMethod) + staticCompilationVisitor.visitMethod(getEntityInstanceMethod) + + // add method: EntityEntry $$_hibernate_getEntityEntry() + def getEntityEntryMethod = new MethodNode( + '$$_hibernate_getEntityEntry', + Modifier.PUBLIC, + entityEntryClassNode, + AstUtils.ZERO_PARAMETERS, + null, + returnS(varX(entityEntryHolderField)) + ) + classNode.addMethod(getEntityEntryMethod) + AnnotatedNodeUtils.markAsGenerated(classNode, getEntityEntryMethod) + staticCompilationVisitor.visitMethod(getEntityEntryMethod) + + // add method: void $$_hibernate_setEntityEntry(EntityEntry entityEntry) + def entityEntryParam = param(entityEntryClassNode, 'entityEntry') + def setEntityEntryMethod = new MethodNode( + '$$_hibernate_setEntityEntry', + Modifier.PUBLIC, + ClassHelper.VOID_TYPE, + params(entityEntryParam), + null, + assignS(varX(entityEntryHolderField), varX(entityEntryParam)) + ) + classNode.addMethod(setEntityEntryMethod) + AnnotatedNodeUtils.markAsGenerated(classNode, setEntityEntryMethod) + staticCompilationVisitor.visitMethod(setEntityEntryMethod) + + // add method: ManagedEntity $$_hibernate_getPreviousManagedEntity() + def getPreviousManagedEntityMethod = new MethodNode( + '$$_hibernate_getPreviousManagedEntity', + Modifier.PUBLIC, + managedEntityClassNode, + AstUtils.ZERO_PARAMETERS, + null, + returnS(varX(previousManagedEntityField)) + ) + classNode.addMethod(getPreviousManagedEntityMethod) + AnnotatedNodeUtils.markAsGenerated(classNode, getPreviousManagedEntityMethod) + staticCompilationVisitor.visitMethod(getPreviousManagedEntityMethod) + + // add method: ManagedEntity $$_hibernate_getNextManagedEntity() { + def getNextManagedEntityMethod = new MethodNode( + '$$_hibernate_getNextManagedEntity', + Modifier.PUBLIC, + managedEntityClassNode, + AstUtils.ZERO_PARAMETERS, + null, + returnS(varX(nextManagedEntityField)) + ) + classNode.addMethod(getNextManagedEntityMethod) + AnnotatedNodeUtils.markAsGenerated(classNode, getNextManagedEntityMethod) + staticCompilationVisitor.visitMethod(getNextManagedEntityMethod) + + // add method: void $$_hibernate_setPreviousManagedEntity(ManagedEntity previous) + def previousParam = param(managedEntityClassNode, 'previous') + def setPreviousManagedEntityMethod = new MethodNode( + '$$_hibernate_setPreviousManagedEntity', + Modifier.PUBLIC, + ClassHelper.VOID_TYPE, + params(previousParam), + null, + assignS(varX(previousManagedEntityField), varX(previousParam)) + ) + classNode.addMethod(setPreviousManagedEntityMethod) + AnnotatedNodeUtils.markAsGenerated(classNode, setPreviousManagedEntityMethod) + staticCompilationVisitor.visitMethod(setPreviousManagedEntityMethod) + + // add method: void $$_hibernate_setNextManagedEntity(ManagedEntity next) + def nextParam = param(managedEntityClassNode, 'next') + def setNextManagedEntityMethod = new MethodNode( + '$$_hibernate_setNextManagedEntity', + Modifier.PUBLIC, + ClassHelper.VOID_TYPE, + params(nextParam), + null, + assignS(varX(nextManagedEntityField), varX(nextParam)) + ) + classNode.addMethod(setNextManagedEntityMethod) + AnnotatedNodeUtils.markAsGenerated(classNode, setNextManagedEntityMethod) + staticCompilationVisitor.visitMethod(setNextManagedEntityMethod) + + List allMethods = classNode.getMethods() + for (MethodNode methodNode in allMethods) { + if (methodNode.getAnnotations(ClassHelper.make(DirtyCheckedProperty))) { + if (AstUtils.isGetter(methodNode)) { + def codeVisitor = new ClassCodeVisitorSupport() { + @Override + protected SourceUnit getSourceUnit() { + return sourceUnit + } + + @Override + void visitReturnStatement(ReturnStatement statement) { + ReturnStatement rs = (ReturnStatement) statement + def i = varX(interceptorField) + def propertyName = NameUtils.getPropertyNameForGetterOrSetter(methodNode.getName()) + + def returnType = methodNode.getReturnType() + final boolean isPrimitive = ClassHelper.isPrimitiveType(returnType) + String readMethodName = isPrimitive ? "read${NameUtils.capitalize(returnType.getName())}" : 'readObject' + def readObjectCall = callX(i, readMethodName, args(varX('this'), constX(propertyName), rs.getExpression())) + def ternaryExpr = ternaryX( + equalsNullX(varX(interceptorField)), + rs.getExpression(), + readObjectCall + ) + staticCompilationVisitor.visitTernaryExpression(ternaryExpr) + rs.setExpression(ternaryExpr) + + } + } + codeVisitor.visitMethod(methodNode) + } + else { + Statement code = methodNode.code + if (code instanceof BlockStatement) { + BlockStatement bs = (BlockStatement) code + Parameter parameter = methodNode.getParameters()[0] + ClassNode parameterType = parameter.type + final boolean isPrimitive = ClassHelper.isPrimitiveType(parameterType) + String writeMethodName = isPrimitive ? "write${NameUtils.capitalize(parameterType.getName())}" : 'writeObject' + String propertyName = NameUtils.getPropertyNameForGetterOrSetter(methodNode.getName()) + def interceptorFieldExpr = fieldX(interceptorField) + def ifStatement = ifS(neX(interceptorFieldExpr, constX(null)), + assignS( + varX(parameter), + callX(interceptorFieldExpr, writeMethodName, args(varX('this'), constX(propertyName), propX(varX('this'), propertyName), varX(parameter))) + ) + ) + staticCompilationVisitor.visitIfElse((IfStatement) ifStatement) + bs.getStatements().add(0, ifStatement) + } + } + + } + } + + classNode.putNodeMetaData(AstUtils.TRANSFORM_APPLIED_MARKER, APPLIED_MARKER) + } + + @Override + int priority() { + GroovyTransformOrder.HIBERNATE5_ORDER + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/AbstractHibernateConnectionSourceFactory.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/AbstractHibernateConnectionSourceFactory.java new file mode 100644 index 00000000000..598afb3c9b9 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/AbstractHibernateConnectionSourceFactory.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.connections; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; + +import javax.sql.DataSource; + +import org.hibernate.SessionFactory; + +import org.springframework.core.env.PropertyResolver; + +import org.grails.datastore.gorm.jdbc.connections.CachedDataSourceConnectionSourceFactory; +import org.grails.datastore.gorm.jdbc.connections.DataSourceConnectionSourceFactory; +import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings; +import org.grails.datastore.gorm.jdbc.connections.DataSourceSettingsBuilder; +import org.grails.datastore.mapping.core.connections.AbstractConnectionSourceFactory; +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings; +import org.grails.orm.hibernate.cfg.Settings; + +/** + * Constructs a Hibernate {@link SessionFactory} + * + * @author Graeme Rocher + * @since 6.0 + */ +public abstract class AbstractHibernateConnectionSourceFactory extends AbstractConnectionSourceFactory { + + protected DataSourceConnectionSourceFactory dataSourceConnectionSourceFactory = new CachedDataSourceConnectionSourceFactory(); + + /** + * Sets the factory for creating SQL {@link DataSource} connection sources + * + * @param dataSourceConnectionSourceFactory + */ + public void setDataSourceConnectionSourceFactory(DataSourceConnectionSourceFactory dataSourceConnectionSourceFactory) { + this.dataSourceConnectionSourceFactory = dataSourceConnectionSourceFactory; + } + + public ConnectionSource create(String name, HibernateConnectionSourceSettings settings) { + DataSourceSettings dataSourceSettings = settings.getDataSource(); + ConnectionSource dataSourceConnectionSource = dataSourceConnectionSourceFactory.create(name, dataSourceSettings); + return create(name, dataSourceConnectionSource, settings); + } + + @Override + public Serializable getConnectionSourcesConfigurationKey() { + return Settings.SETTING_DATASOURCES; + } + + @Override + public HibernateConnectionSourceSettings buildRuntimeSettings(String name, PropertyResolver configuration, F fallbackSettings) { + return buildSettingsWithPrefix(configuration, fallbackSettings, ""); + } + + /** + * Creates a ConnectionSource for the given DataSource + * + * @param name The name + * @param dataSourceConnectionSource The data source connection source + * @param settings The settings + * @return The ConnectionSource + */ + public abstract ConnectionSource create(String name, ConnectionSource dataSourceConnectionSource, HibernateConnectionSourceSettings settings); + + protected HibernateConnectionSourceSettings buildSettings(String name, PropertyResolver configuration, F fallbackSettings, boolean isDefaultDataSource) { + HibernateConnectionSourceSettingsBuilder builder; + HibernateConnectionSourceSettings settings; + if (isDefaultDataSource) { + String qualified = Settings.SETTING_DATASOURCES + '.' + Settings.SETTING_DATASOURCE; + builder = new HibernateConnectionSourceSettingsBuilder(configuration, "", fallbackSettings); + Map config = configuration.getProperty(qualified, Map.class, Collections.emptyMap()); + settings = builder.build(); + if (!config.isEmpty()) { + + DataSourceSettings dsfallbackSettings = null; + if (fallbackSettings instanceof HibernateConnectionSourceSettings) { + dsfallbackSettings = ((HibernateConnectionSourceSettings) fallbackSettings).getDataSource(); + } + else if (fallbackSettings instanceof DataSourceSettings) { + dsfallbackSettings = (DataSourceSettings) fallbackSettings; + } + DataSourceSettingsBuilder dataSourceSettingsBuilder = new DataSourceSettingsBuilder(configuration, qualified, dsfallbackSettings); + DataSourceSettings dataSourceSettings = dataSourceSettingsBuilder.build(); + settings.setDataSource(dataSourceSettings); + } + } + else { + String prefix = Settings.SETTING_DATASOURCES + "." + name; + settings = buildSettingsWithPrefix(configuration, fallbackSettings, prefix); + } + return settings; + } + + private HibernateConnectionSourceSettings buildSettingsWithPrefix(PropertyResolver configuration, F fallbackSettings, String prefix) { + HibernateConnectionSourceSettingsBuilder builder; + HibernateConnectionSourceSettings settings; + builder = new HibernateConnectionSourceSettingsBuilder(configuration, prefix, fallbackSettings); + + DataSourceSettings dsfallbackSettings = null; + if (fallbackSettings instanceof HibernateConnectionSourceSettings) { + dsfallbackSettings = ((HibernateConnectionSourceSettings) fallbackSettings).getDataSource(); + } + else if (fallbackSettings instanceof DataSourceSettings) { + dsfallbackSettings = (DataSourceSettings) fallbackSettings; + } + + settings = builder.build(); + if (prefix.length() == 0) { + // if the prefix is zero length then this is a datasource added at runtime using ConnectionSources.addConnectionSource + DataSourceSettingsBuilder dataSourceSettingsBuilder = new DataSourceSettingsBuilder(configuration, prefix, dsfallbackSettings); + DataSourceSettings dataSourceSettings = dataSourceSettingsBuilder.build(); + settings.setDataSource(dataSourceSettings); + } + else { + if (configuration.getProperty(prefix + ".dataSource", Map.class, Collections.emptyMap()).isEmpty()) { + DataSourceSettingsBuilder dataSourceSettingsBuilder = new DataSourceSettingsBuilder(configuration, prefix, dsfallbackSettings); + DataSourceSettings dataSourceSettings = dataSourceSettingsBuilder.build(); + settings.setDataSource(dataSourceSettings); + } + } + return settings; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSource.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSource.java new file mode 100644 index 00000000000..700dfc01306 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSource.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.connections; + +import java.io.IOException; + +import javax.sql.DataSource; + +import org.hibernate.SessionFactory; + +import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings; +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.datastore.mapping.core.connections.DefaultConnectionSource; + +/** + * + * Implements the {@link org.grails.datastore.mapping.core.connections.ConnectionSource} interface for Hibernate + * + * @author Graeme Rocher + * @since 6.0 + */ +public class HibernateConnectionSource extends DefaultConnectionSource { + + protected final ConnectionSource dataSource; + + public HibernateConnectionSource(String name, SessionFactory sessionFactory, ConnectionSource dataSourceConnectionSource, HibernateConnectionSourceSettings settings) { + super(name, sessionFactory, settings); + this.dataSource = dataSourceConnectionSource; + } + + @Override + public void close() throws IOException { + super.close(); + try { + SessionFactory sessionFactory = getSource(); + sessionFactory.close(); + } finally { + if (dataSource != null) { + dataSource.close(); + } + } + } + + /** + * @return The underlying SQL {@link DataSource} + */ + public DataSource getDataSource() { + return dataSource.getSource(); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactory.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactory.java new file mode 100644 index 00000000000..7e56d36b07e --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactory.java @@ -0,0 +1,287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.connections; + +import java.io.File; +import java.io.IOException; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.hibernate.Interceptor; +import org.hibernate.SessionFactory; +import org.hibernate.boot.spi.MetadataContributor; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.NamingStrategy; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.core.io.Resource; + +import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings; +import org.grails.datastore.gorm.jdbc.connections.SpringDataSourceConnectionSourceFactory; +import org.grails.datastore.gorm.validation.jakarta.JakartaValidatorRegistry; +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.datastore.mapping.core.exceptions.ConfigurationException; +import org.grails.datastore.mapping.core.grailsversion.GrailsVersion; +import org.grails.datastore.mapping.validation.ValidatorRegistry; +import org.grails.orm.hibernate.HibernateEventListeners; +import org.grails.orm.hibernate.cfg.GrailsDomainBinder; +import org.grails.orm.hibernate.cfg.HibernateMappingContext; +import org.grails.orm.hibernate.cfg.HibernateMappingContextConfiguration; +import org.grails.orm.hibernate.support.AbstractClosureEventTriggeringInterceptor; +import org.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor; + +/** + * Constructs {@link SessionFactory} instances from a {@link HibernateMappingContext} + * + * @author Graeme Rocher + * @since 6.0 + */ +public class HibernateConnectionSourceFactory extends AbstractHibernateConnectionSourceFactory implements ApplicationContextAware, MessageSourceAware { + + static { + // use Slf4j logging by default + System.setProperty("org.jboss.logging.provider", "slf4j"); + } + + protected HibernateMappingContext mappingContext; + protected Class[] persistentClasses = new Class[0]; + private ApplicationContext applicationContext; + protected HibernateEventListeners hibernateEventListeners; + protected Interceptor interceptor; + protected MetadataContributor metadataContributor; + protected MessageSource messageSource = new StaticMessageSource(); + + public HibernateConnectionSourceFactory(Class... classes) { + this.persistentClasses = classes; + } + + public Class[] getPersistentClasses() { + return persistentClasses; + } + + @Autowired(required = false) + public void setHibernateEventListeners(HibernateEventListeners hibernateEventListeners) { + this.hibernateEventListeners = hibernateEventListeners; + } + + @Autowired(required = false) + public void setInterceptor(Interceptor interceptor) { + this.interceptor = interceptor; + } + + @Autowired(required = false) + public void setMetadataContributor(MetadataContributor metadataContributor) { + this.metadataContributor = metadataContributor; + } + + public HibernateMappingContext getMappingContext() { + return mappingContext; + } + + @Override + public ConnectionSource create(String name, ConnectionSource dataSourceConnectionSource, HibernateConnectionSourceSettings settings) { + HibernateMappingContextConfiguration configuration = buildConfiguration(name, dataSourceConnectionSource, settings); + SessionFactory sessionFactory = configuration.buildSessionFactory(); + return new HibernateConnectionSource(name, sessionFactory, dataSourceConnectionSource, settings); + } + + public HibernateMappingContextConfiguration buildConfiguration(String name, ConnectionSource dataSourceConnectionSource, HibernateConnectionSourceSettings settings) { + boolean isDefault = ConnectionSource.DEFAULT.equals(name); + + if (mappingContext == null) { + mappingContext = new HibernateMappingContext(settings, applicationContext, persistentClasses); + } + + HibernateConnectionSourceSettings.HibernateSettings hibernateSettings = settings.getHibernate(); + Class configClass = hibernateSettings.getConfigClass(); + + HibernateMappingContextConfiguration configuration; + if (configClass != null) { + if (!HibernateMappingContextConfiguration.class.isAssignableFrom(configClass)) { + throw new ConfigurationException("The configClass setting must be a subclass for [HibernateMappingContextConfiguration]"); + } + else { + configuration = (HibernateMappingContextConfiguration) BeanUtils.instantiateClass(configClass); + } + } + else { + configuration = new HibernateMappingContextConfiguration(); + } + + if (JakartaValidatorRegistry.isAvailable() && messageSource != null) { + ValidatorRegistry registry = new JakartaValidatorRegistry(mappingContext, dataSourceConnectionSource.getSettings(), messageSource); + mappingContext.setValidatorRegistry(registry); + configuration.getProperties().put("jakarta.persistence.validation.factory", registry); + } + + if (applicationContext != null && applicationContext.containsBean(dataSourceConnectionSource.getName())) { + configuration.setApplicationContext(this.applicationContext); + } + else { + configuration.setDataSourceConnectionSource(dataSourceConnectionSource); + } + + Resource[] configLocations = hibernateSettings.getConfigLocations(); + if (configLocations != null) { + for (Resource resource : configLocations) { + // Load Hibernate configuration from given location. + try { + configuration.configure(resource.getURL()); + } catch (IOException e) { + throw new ConfigurationException("Cannot configure Hibernate config for location: " + resource.getFilename(), e); + } + } + } + + Resource[] mappingLocations = hibernateSettings.getMappingLocations(); + if (mappingLocations != null) { + // Register given Hibernate mapping definitions, contained in resource files. + for (Resource resource : mappingLocations) { + try { + configuration.addInputStream(resource.getInputStream()); + } catch (IOException e) { + throw new ConfigurationException("Cannot configure Hibernate config for location: " + resource.getFilename(), e); + } + } + } + + Resource[] cacheableMappingLocations = hibernateSettings.getCacheableMappingLocations(); + if (cacheableMappingLocations != null) { + // Register given cacheable Hibernate mapping definitions, read from the file system. + for (Resource resource : cacheableMappingLocations) { + try { + configuration.addCacheableFile(resource.getFile()); + } catch (IOException e) { + throw new ConfigurationException("Cannot configure Hibernate config for location: " + resource.getFilename(), e); + } + } + } + + Resource[] mappingJarLocations = hibernateSettings.getMappingJarLocations(); + if (mappingJarLocations != null) { + // Register given Hibernate mapping definitions, contained in jar files. + for (Resource resource : mappingJarLocations) { + try { + configuration.addJar(resource.getFile()); + } catch (IOException e) { + throw new ConfigurationException("Cannot configure Hibernate config for location: " + resource.getFilename(), e); + } + } + } + + Resource[] mappingDirectoryLocations = hibernateSettings.getMappingDirectoryLocations(); + if (mappingDirectoryLocations != null) { + // Register all Hibernate mapping definitions in the given directories. + for (Resource resource : mappingDirectoryLocations) { + File file; + try { + file = resource.getFile(); + } catch (IOException e) { + throw new ConfigurationException("Cannot configure Hibernate config for location: " + resource.getFilename(), e); + } + if (!file.isDirectory()) { + throw new IllegalArgumentException("Mapping directory location [" + resource + "] does not denote a directory"); + } + configuration.addDirectory(file); + } + } + + if (this.interceptor != null) { + configuration.setInterceptor(this.interceptor); + } + + if (this.metadataContributor != null) { + configuration.setMetadataContributor(metadataContributor); + } + + Class[] annotatedClasses = hibernateSettings.getAnnotatedClasses(); + if (annotatedClasses != null) { + configuration.addAnnotatedClasses(annotatedClasses); + } + + String[] annotatedPackages = hibernateSettings.getAnnotatedPackages(); + if (annotatedPackages != null) { + configuration.addPackages(annotatedPackages); + } + + String[] packagesToScan = hibernateSettings.getPackagesToScan(); + if (packagesToScan != null) { + configuration.scanPackages(packagesToScan); + } + + Class closureEventTriggeringInterceptorClass = hibernateSettings.getClosureEventTriggeringInterceptorClass(); + + AbstractClosureEventTriggeringInterceptor eventTriggeringInterceptor; + + if (closureEventTriggeringInterceptorClass == null) { + eventTriggeringInterceptor = new ClosureEventTriggeringInterceptor(); + } + else { + eventTriggeringInterceptor = BeanUtils.instantiateClass(closureEventTriggeringInterceptorClass); + } + + hibernateSettings.setEventTriggeringInterceptor(eventTriggeringInterceptor); + + try { + Class namingStrategy = hibernateSettings.getNaming_strategy(); + if (namingStrategy != null) { + GrailsDomainBinder.configureNamingStrategy(name, namingStrategy); + } + } catch (Throwable e) { + throw new ConfigurationException("Error configuring naming strategy: " + e.getMessage(), e); + } + + configuration.setEventListeners(hibernateSettings.toHibernateEventListeners(eventTriggeringInterceptor)); + HibernateEventListeners hibernateEventListeners = hibernateSettings.getHibernateEventListeners(); + configuration.setHibernateEventListeners(this.hibernateEventListeners != null ? this.hibernateEventListeners : hibernateEventListeners); + configuration.setHibernateMappingContext(mappingContext); + configuration.setDataSourceName(name); + configuration.setSessionFactoryBeanName(isDefault ? "sessionFactory" : "sessionFactory_" + name); + Properties hibernateProperties = settings.toProperties(); + configuration.addProperties(hibernateProperties); + return configuration; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (applicationContext != null) { + this.applicationContext = applicationContext; + this.messageSource = applicationContext; + if (!GrailsVersion.isAtLeastMajorMinor(3, 3)) { + SpringDataSourceConnectionSourceFactory springDataSourceConnectionSourceFactory = new SpringDataSourceConnectionSourceFactory(); + springDataSourceConnectionSourceFactory.setApplicationContext(applicationContext); + this.dataSourceConnectionSourceFactory = springDataSourceConnectionSourceFactory; + } + } + } + + @Override + public void setMessageSource(MessageSource messageSource) { + this.messageSource = messageSource; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettings.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettings.groovy new file mode 100644 index 00000000000..0c9aab26a2d --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettings.groovy @@ -0,0 +1,341 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.connections + +import groovy.transform.AutoClone +import groovy.transform.CompileStatic +import groovy.transform.builder.Builder +import groovy.transform.builder.SimpleStrategy + +import org.hibernate.CustomEntityDirtinessStrategy +import org.hibernate.cfg.AvailableSettings +import org.hibernate.cfg.Configuration +import org.hibernate.cfg.ImprovedNamingStrategy +import org.hibernate.cfg.NamingStrategy + +import org.springframework.core.io.Resource + +import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings +import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings +import org.grails.orm.hibernate.HibernateEventListeners +import org.grails.orm.hibernate.dirty.GrailsEntityDirtinessStrategy +import org.grails.orm.hibernate.support.AbstractClosureEventTriggeringInterceptor + +/** + * Settings for Hibernate + * + * @author Graeme Rocher + * @since 6.0 + */ +@Builder(builderStrategy = SimpleStrategy, prefix = '') +@AutoClone +class HibernateConnectionSourceSettings extends ConnectionSourceSettings { + + /** + * Whether to prepare the datastore for runtime reloading + */ + boolean enableReload = false + /** + * Settings for the dataSource + */ + DataSourceSettings dataSource = new DataSourceSettings() + + /** + * Settings for Hibernate + */ + HibernateSettings hibernate = new HibernateSettings() + + /** + * Convert to hibernate properties + * + * @return The properties + */ + Properties toProperties() { + Properties properties = new Properties() + properties.putAll(dataSource.toHibernateProperties()) + properties.putAll(hibernate.toProperties()) + return properties + } + + @Builder(builderStrategy = SimpleStrategy, prefix = '') + @AutoClone + static class HibernateSettings extends LinkedHashMap { + + /** + * Whether OpenSessionInView should be read-only + */ + OsivSettings osiv = new OsivSettings() + /** + * Whether Hibernate should be in read-only mode + */ + boolean readOnly = false + + /** + * Whether to use Hibernate's dirty checking instead of Grails' + */ + boolean hibernateDirtyChecking = false + /** + * Cache settings + */ + CacheSettings cache = new CacheSettings() + + /** + * Flush settings + */ + FlushSettings flush = new FlushSettings() + + /** + * The configuration class + */ + Class configClass + + /** + * The naming strategy + */ + Class naming_strategy = ImprovedNamingStrategy + + /** + * + */ + Class entity_dirtiness_strategy = GrailsEntityDirtinessStrategy + + /** + * A subclass of AbstractClosureEventTriggeringInterceptor + */ + Class closureEventTriggeringInterceptorClass + + /** + * The event triggering interceptor + */ + AbstractClosureEventTriggeringInterceptor eventTriggeringInterceptor + /** + * The default hibernate event listeners + */ + HibernateEventListeners hibernateEventListeners = new HibernateEventListeners() + + /** + * Set the locations of multiple Hibernate XML config files, for example as + * classpath resources "classpath:hibernate.cfg.xml,classpath:extension.cfg.xml". + *

Note: Can be omitted when all necessary properties and mapping + * resources are specified locally via this bean. + * @see org.hibernate.cfg.Configuration#configure(java.net.URL) + */ + + Resource[] configLocations + + /** + * Set locations of Hibernate mapping files, for example as classpath + * resource "classpath:example.hbm.xml". Supports any resource location + * via Spring's resource abstraction, for example relative paths like + * "WEB-INF/mappings/example.hbm.xml" when running in an application context. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addInputStream + */ + Resource[] mappingLocations + + /** + * Set locations of cacheable Hibernate mapping files, for example as web app + * resource "/WEB-INF/mapping/example.hbm.xml". Supports any resource location + * via Spring's resource abstraction, as long as the resource can be resolved + * in the file system. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addCacheableFile(java.io.File) + */ + Resource[] cacheableMappingLocations + + /** + * Set locations of jar files that contain Hibernate mapping resources, + * like "WEB-INF/lib/example.hbm.jar". + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addJar(java.io.File) + */ + Resource[] mappingJarLocations + + /** + * Set locations of directories that contain Hibernate mapping resources, + * like "WEB-INF/mappings". + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addDirectory(java.io.File) + */ + Resource[] mappingDirectoryLocations + + /** + * Specify annotated entity classes to register with this Hibernate SessionFactory. + * @see org.hibernate.cfg.Configuration#addAnnotatedClass(Class) + */ + Class[] annotatedClasses + + /** + * Specify the names of annotated packages, for which package-level + * annotation metadata will be read. + * @see org.hibernate.cfg.Configuration#addPackage(String) + */ + String[] annotatedPackages + + /** + * Specify packages to search for autodetection of your entity classes in the + * classpath. This is analogous to Spring's component-scan feature + * ({@link org.springframework.context.annotation.ClassPathBeanDefinitionScanner}). + */ + String[] packagesToScan + + /** + * Any additional properties that should be passed through as is. + */ + Properties additionalProperties = new Properties() + + @CompileStatic + Map toHibernateEventListeners(AbstractClosureEventTriggeringInterceptor eventTriggeringInterceptor) { + if (eventTriggeringInterceptor != null) { + return [ + 'save': eventTriggeringInterceptor, + 'save-update': eventTriggeringInterceptor, + 'pre-load': eventTriggeringInterceptor, + 'post-load': eventTriggeringInterceptor, + 'pre-insert': eventTriggeringInterceptor, + 'post-insert': eventTriggeringInterceptor, + 'pre-update': eventTriggeringInterceptor, + 'post-update': eventTriggeringInterceptor, + 'pre-delete': eventTriggeringInterceptor, + 'post-delete': eventTriggeringInterceptor + ] as Map + } + return Collections.emptyMap() + } + + /** + * Convert to Hibernate properties + * + * @return The hibernate properties + */ + @CompileStatic + Properties toProperties() { + Properties props = new Properties() + if (naming_strategy != null) { + props.put('hibernate.naming_strategy', naming_strategy.name) + } + if (configClass != null) { + props.put('hibernate.config_class', configClass.name) + } + props.put('hibernate.use_query_cache', String.valueOf(cache.queries)) + + if (entity_dirtiness_strategy != null && !hibernateDirtyChecking) { + props.put('hibernate.entity_dirtiness_strategy', entity_dirtiness_strategy.name) + } + + // Hibernate 5.1/5.2: manually enforce connection release mode ON_CLOSE (the former default) + try { + // Try Hibernate 5.2 + AvailableSettings.getField('CONNECTION_HANDLING') + props.put('hibernate.connection.handling_mode', 'DELAYED_ACQUISITION_AND_HOLD') + } + catch (NoSuchFieldException ex) { + // Try Hibernate 5.1 + try { + AvailableSettings.getField('ACQUIRE_CONNECTIONS') + props.put('hibernate.connection.release_mode', 'ON_CLOSE') + } + catch (NoSuchFieldException ex2) { + // on Hibernate 5.0.x or lower - no need to change the default there + } + } + + String prefix = 'hibernate' + props.putAll(additionalProperties) + populateProperties(props, this, prefix) + return props + } + + @CompileStatic + protected void populateProperties(Properties props, Map current, String prefix) { + for (key in current.keySet()) { + def value = current.get(key) + if (value instanceof Map) { + populateProperties(props, (Map) value, "${prefix}.$key") + } + else { + props.put("$prefix.$key".toString(), value) + } + } + } + + @Builder(builderStrategy = SimpleStrategy, prefix = '') + @AutoClone + static class CacheSettings { + /** + * Whether to cache queries + */ + boolean queries = false + } + + @Builder(builderStrategy = SimpleStrategy, prefix = '') + @AutoClone + static class FlushSettings { + /** + * The default flush mode + */ + FlushMode mode = FlushMode.COMMIT + + /** + * We use a separate enum here because the classes differ between Hibernate 3 and 4 + * + * @see org.hibernate.FlushMode + */ + static enum FlushMode { + MANUAL(0), + COMMIT(5), + AUTO(10), + ALWAYS(20) + + private final int level + + FlushMode(int level) { + this.level = level + } + + int getLevel() { + return level + } + } + } + + /** + * Settings for OpenSessionInView + */ + @Builder(builderStrategy = SimpleStrategy, prefix = '') + @AutoClone + static class OsivSettings { + /** + * Whether to cache queries + */ + boolean readonly = false + + /** + * Whether OSIV is enabled + */ + boolean enabled = true + } + + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettingsBuilder.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettingsBuilder.groovy new file mode 100644 index 00000000000..b9239d5cd8a --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettingsBuilder.groovy @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.connections + +import groovy.transform.CompileStatic + +import org.springframework.core.env.PropertyResolver + +import org.grails.datastore.mapping.config.ConfigurationBuilder +import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings + +/** + * Builds the GORM for Hibernate configuration + * + * @author Graeme Rocher + * @since 6.0 + */ +@CompileStatic +class HibernateConnectionSourceSettingsBuilder extends ConfigurationBuilder { + + HibernateConnectionSourceSettings fallBackHibernateSettings + + HibernateConnectionSourceSettingsBuilder(PropertyResolver propertyResolver, String configurationPrefix = '', ConnectionSourceSettings fallBackConfiguration = null) { + super(propertyResolver, configurationPrefix, fallBackConfiguration) + + if (fallBackConfiguration instanceof HibernateConnectionSourceSettings) { + fallBackHibernateSettings = (HibernateConnectionSourceSettings) fallBackConfiguration + } + } + + @Override + protected HibernateConnectionSourceSettings createBuilder() { + def settings = new HibernateConnectionSourceSettings() + if (fallBackHibernateSettings != null) { + settings.getHibernate().putAll(fallBackHibernateSettings.getHibernate()) + } + return settings + } + + @Override + HibernateConnectionSourceSettings build() { + HibernateConnectionSourceSettings finalSettings = (HibernateConnectionSourceSettings) super.build() + Map orgHibernateProperties = propertyResolver.getProperty('org.hibernate', Map, Collections.emptyMap()) + Properties additionalProperties = finalSettings.getHibernate().getAdditionalProperties() + for (key in orgHibernateProperties.keySet()) { + additionalProperties.put("org.hibernate.$key".toString(), orgHibernateProperties.get(key)) + } + return finalSettings + } + + @Override + protected HibernateConnectionSourceSettings toConfiguration(HibernateConnectionSourceSettings builder) { + return builder + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/datasource/MultipleDataSourceSupport.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/datasource/MultipleDataSourceSupport.java new file mode 100644 index 00000000000..b312dd2d272 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/datasource/MultipleDataSourceSupport.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.datasource; + +import java.util.List; + +import org.grails.datastore.mapping.core.connections.ConnectionSourcesSupport; +import org.grails.datastore.mapping.model.PersistentEntity; + +/** + * Support methods for Multiple data source handling + * + * @author Graeme Rocher + * @since 5.0.2 + */ +public class MultipleDataSourceSupport { + /** + * If a domain class uses more than one datasource, we need to know which one to use + * when calling a method without a namespace qualifier. + * + * @param domainClass the domain class + * @return the default datasource name + */ + public static String getDefaultDataSource(PersistentEntity domainClass) { + return ConnectionSourcesSupport.getDefaultConnectionSourceName(domainClass); + } + + public static List getDatasourceNames(PersistentEntity domainClass) { + return ConnectionSourcesSupport.getConnectionSourceNames(domainClass); + } + + public static boolean usesDatasource(PersistentEntity domainClass, String dataSourceName) { + return ConnectionSourcesSupport.usesConnectionSource(domainClass, dataSourceName); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/dirty/GrailsEntityDirtinessStrategy.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/dirty/GrailsEntityDirtinessStrategy.groovy new file mode 100644 index 00000000000..93c88d34523 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/dirty/GrailsEntityDirtinessStrategy.groovy @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.dirty + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import org.hibernate.CustomEntityDirtinessStrategy +import org.hibernate.Hibernate +import org.hibernate.Session +import org.hibernate.engine.spi.SessionImplementor +import org.hibernate.engine.spi.Status +import org.hibernate.persister.entity.EntityPersister +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.mapping.dirty.checking.DirtyCheckable +import org.grails.datastore.mapping.dirty.checking.DirtyCheckingSupport +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.config.GormProperties +import org.grails.datastore.mapping.model.types.Embedded + +/** + * A class to customize Hibernate dirtiness based on Grails {@link DirtyCheckable} interface + * + * @author James Kleeh + * @author Graeme Rocher + * + * @since 6.0.3 + */ +@CompileStatic +class GrailsEntityDirtinessStrategy implements CustomEntityDirtinessStrategy { + + protected static final Logger LOG = LoggerFactory.getLogger(GrailsEntityDirtinessStrategy) + + @Override + boolean canDirtyCheck(Object entity, EntityPersister persister, Session session) { + return entity instanceof DirtyCheckable + } + + @Override + boolean isDirty(Object entity, EntityPersister persister, Session session) { + !session.contains(entity) || cast(entity).hasChanged() || DirtyCheckingSupport.areEmbeddedDirty(GormEnhancer.findEntity(Hibernate.getClass(entity)), entity) + } + + @Override + void resetDirty(Object entity, EntityPersister persister, Session session) { + if (canDirtyCheck(entity, persister, session)) { + cast(entity).trackChanges() + try { + PersistentEntity persistentEntity = GormEnhancer.findEntity(Hibernate.getClass(entity)) + if (persistentEntity != null) { + resetDirtyEmbeddedObjects(persistentEntity, entity, persister, session) + } + } catch (IllegalStateException e) { + if (LOG.isDebugEnabled()) { + LOG.debug(e.message, e) + } + } + } + } + + private void resetDirtyEmbeddedObjects(PersistentEntity persistentEntity, + Object entity, + EntityPersister persister, + Session session) { + + if (DirtyCheckingSupport.areEmbeddedDirty(persistentEntity, entity)) { + final associations = persistentEntity.getEmbedded() + for (Embedded a in associations) { + final value = a.reader.read(entity) + resetDirty(value, persister, session) + } + } + } + + @Override + void findDirty(Object entity, EntityPersister persister, Session session, CustomEntityDirtinessStrategy.DirtyCheckContext dirtyCheckContext) { + Status status = getStatus(session, entity) + if (entity instanceof DirtyCheckable) { + dirtyCheckContext.doDirtyChecking( + new CustomEntityDirtinessStrategy.AttributeChecker() { + @Override + boolean isDirty(CustomEntityDirtinessStrategy.AttributeInformation attributeInformation) { + String propertyName = attributeInformation.name + if (status != null) { + if (status == Status.MANAGED) { + // perform dirty check + DirtyCheckable dirtyCheckable = cast(entity) + if (GormProperties.LAST_UPDATED == propertyName) { + return dirtyCheckable.hasChanged() + } + else { + if (dirtyCheckable.hasChanged(propertyName)) { + return true + } + else { + PersistentEntity gormEntity = GormEnhancer.findEntity(Hibernate.getClass(entity)) + PersistentProperty prop = gormEntity.getPropertyByName(attributeInformation.name) + if (prop instanceof Embedded) { + def val = prop.reader.read(entity) + if (val instanceof DirtyCheckable) { + return ((DirtyCheckable) val).hasChanged() + } + else { + return false + } + } + else { + return false + } + } + } + } + else { + // either deleted or in a state that cannot be regarded as dirty + return false + } + } + else { + // a new object not within the session + return true + } + } + } + ) + } + } + + @CompileDynamic + Status getStatus(Session session, Object entity) { + SessionImplementor si = (SessionImplementor) session + return si.getPersistenceContext().getEntry(entity)?.getStatus() + } + + private DirtyCheckable cast(Object entity) { + return DirtyCheckable.cast(entity) + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/AbstractHibernateEventListener.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/AbstractHibernateEventListener.java new file mode 100644 index 00000000000..d716cadf3e3 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/AbstractHibernateEventListener.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.event.listener; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.context.ApplicationEvent; + +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent; +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener; +import org.grails.orm.hibernate.AbstractHibernateDatastore; +import org.grails.orm.hibernate.connections.HibernateConnectionSourceSettings; +import org.grails.orm.hibernate.support.SoftKey; + +/** + *

Invokes closure events on domain entities such as beforeInsert, beforeUpdate and beforeDelete. + * + * @author Graeme Rocher + * @author Lari Hotari + * @author Burt Beckwith + * @since 2.0 + */ +public abstract class AbstractHibernateEventListener extends AbstractPersistenceEventListener { + + protected final transient ConcurrentMap>, Boolean> cachedShouldTrigger = + new ConcurrentHashMap<>(); + protected final boolean failOnError; + protected final List failOnErrorPackages; + + protected AbstractHibernateEventListener(AbstractHibernateDatastore datastore) { + super(datastore); + HibernateConnectionSourceSettings settings = datastore.getConnectionSources().getDefaultConnectionSource().getSettings(); + this.failOnError = settings.isFailOnError(); + this.failOnErrorPackages = settings.getFailOnErrorPackages(); + } + + /** + * {@inheritDoc} + * @see org.springframework.context.event.SmartApplicationListener#supportsEventType( + * java.lang.Class) + */ + public boolean supportsEventType(Class eventType) { + return AbstractPersistenceEvent.class.isAssignableFrom(eventType); + } + + /** + * @return The hibernate datastore + */ + protected AbstractHibernateDatastore getDatastore() { + return (AbstractHibernateDatastore) this.datastore; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListener.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListener.java new file mode 100644 index 00000000000..4b45a17acb6 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListener.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.event.listener; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.hibernate.Hibernate; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.spi.EventSource; +import org.hibernate.event.spi.PostDeleteEvent; +import org.hibernate.event.spi.PostInsertEvent; +import org.hibernate.event.spi.PostLoadEvent; +import org.hibernate.event.spi.PostUpdateEvent; +import org.hibernate.event.spi.PreDeleteEvent; +import org.hibernate.event.spi.PreInsertEvent; +import org.hibernate.event.spi.PreLoadEvent; +import org.hibernate.event.spi.PreUpdateEvent; +import org.hibernate.event.spi.SaveOrUpdateEvent; + +import org.springframework.context.ApplicationEvent; + +import grails.gorm.MultiTenant; +import org.grails.datastore.gorm.timestamp.DefaultTimestampProvider; +import org.grails.datastore.gorm.timestamp.TimestampProvider; +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent; +import org.grails.datastore.mapping.engine.event.ValidationEvent; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.orm.hibernate.AbstractHibernateDatastore; +import org.grails.orm.hibernate.support.ClosureEventListener; +import org.grails.orm.hibernate.support.SoftKey; + +/** + *

Invokes closure events on domain entities such as beforeInsert, beforeUpdate and beforeDelete. + * + * @author Graeme Rocher + * @author Lari Hotari + * @author Burt Beckwith + * @since 2.0 + */ +public class HibernateEventListener extends AbstractHibernateEventListener { + + protected transient ConcurrentMap>, ClosureEventListener> eventListeners = + new ConcurrentHashMap<>(); + + public HibernateEventListener(AbstractHibernateDatastore datastore) { + super(datastore); + } + + @Override + protected void onPersistenceEvent(final AbstractPersistenceEvent event) { + switch (event.getEventType()) { + case PreInsert: + if (onPreInsert((PreInsertEvent) event.getNativeEvent())) { + event.cancel(); + } + break; + case PostInsert: + onPostInsert((PostInsertEvent) event.getNativeEvent()); + break; + case PreUpdate: + if (onPreUpdate((PreUpdateEvent) event.getNativeEvent())) { + event.cancel(); + } + break; + case PostUpdate: + onPostUpdate((PostUpdateEvent) event.getNativeEvent()); + break; + case PreDelete: + if (onPreDelete((PreDeleteEvent) event.getNativeEvent())) { + event.cancel(); + } + break; + case PostDelete: + onPostDelete((PostDeleteEvent) event.getNativeEvent()); + break; + case PreLoad: + onPreLoad((PreLoadEvent) event.getNativeEvent()); + break; + case PostLoad: + onPostLoad((PostLoadEvent) event.getNativeEvent()); + break; + case SaveOrUpdate: + onSaveOrUpdate((SaveOrUpdateEvent) event.getNativeEvent()); + break; + case Validation: + onValidate((ValidationEvent) event); + break; + default: + throw new IllegalStateException("Unexpected EventType: " + event.getEventType()); + } + } + + public void onSaveOrUpdate(SaveOrUpdateEvent event) throws HibernateException { + Object entity = event.getObject(); + if (entity != null) { + ClosureEventListener eventListener; + EventSource session = event.getSession(); + eventListener = findEventListener(entity, (SessionFactoryImplementor) session.getSessionFactory()); + if (eventListener != null) { + eventListener.onSaveOrUpdate(event); + } + } + } + + public void onPreLoad(PreLoadEvent event) { + Object entity = event.getEntity(); + ClosureEventListener eventListener = findEventListener(entity, event.getPersister().getFactory()); + if (eventListener != null) { + eventListener.onPreLoad(event); + } + } + + public void onPostLoad(PostLoadEvent event) { + ClosureEventListener eventListener = findEventListener(event.getEntity(), event.getPersister().getFactory()); + if (eventListener != null) { + eventListener.onPostLoad(event); + } + } + + public void onPostInsert(PostInsertEvent event) { + ClosureEventListener eventListener = findEventListener(event.getEntity(), event.getPersister().getFactory()); + if (eventListener != null) { + eventListener.onPostInsert(event); + } + } + + public boolean onPreInsert(PreInsertEvent event) { + boolean evict = false; + ClosureEventListener eventListener = findEventListener(event.getEntity(), event.getPersister().getFactory()); + if (eventListener != null) { + evict = eventListener.onPreInsert(event); + } + return evict; + } + + public boolean onPreUpdate(PreUpdateEvent event) { + boolean evict = false; + ClosureEventListener eventListener = findEventListener(event.getEntity(), event.getPersister().getFactory()); + if (eventListener != null) { + evict = eventListener.onPreUpdate(event); + } + return evict; + } + + public void onPostUpdate(PostUpdateEvent event) { + ClosureEventListener eventListener = findEventListener(event.getEntity(), event.getPersister().getFactory()); + if (eventListener != null) { + eventListener.onPostUpdate(event); + } + } + + public boolean onPreDelete(PreDeleteEvent event) { + boolean evict = false; + ClosureEventListener eventListener = findEventListener(event.getEntity(), event.getPersister().getFactory()); + if (eventListener != null) { + evict = eventListener.onPreDelete(event); + } + return evict; + } + + public void onPostDelete(PostDeleteEvent event) { + ClosureEventListener eventListener = findEventListener(event.getEntity(), event.getPersister().getFactory()); + if (eventListener != null) { + eventListener.onPostDelete(event); + } + } + + public void onValidate(ValidationEvent event) { + ClosureEventListener eventListener = findEventListener(event.getEntityObject(), null); + if (eventListener != null) { + eventListener.onValidate(event); + } + } + + protected ClosureEventListener findEventListener(Object entity, SessionFactoryImplementor factory) { + if (entity == null) return null; + Class clazz = Hibernate.getClass(entity); + + SoftKey> key = new SoftKey<>(clazz); + ClosureEventListener eventListener = eventListeners.get(key); + if (eventListener != null) { + return eventListener; + } + + Boolean shouldTrigger = cachedShouldTrigger.get(key); + if (shouldTrigger == null || shouldTrigger) { + synchronized (cachedShouldTrigger) { + eventListener = eventListeners.get(key); + if (eventListener == null) { + AbstractHibernateDatastore datastore = getDatastore(); + boolean isValidSessionFactory = MultiTenant.class.isAssignableFrom(clazz) || factory == null || datastore.getSessionFactory().equals(factory); + PersistentEntity persistentEntity = datastore.getMappingContext().getPersistentEntity(clazz.getName()); + shouldTrigger = (persistentEntity != null && isValidSessionFactory); + if (shouldTrigger) { + eventListener = new ClosureEventListener(persistentEntity, failOnError, failOnErrorPackages); + ClosureEventListener previous = eventListeners.putIfAbsent(key, eventListener); + if (previous != null) { + eventListener = previous; + } + } + cachedShouldTrigger.put(key, shouldTrigger); + } + } + } + return eventListener; + } + + /** + * {@inheritDoc} + * @see org.springframework.context.event.SmartApplicationListener#supportsEventType(java.lang.Class) + */ + public boolean supportsEventType(Class eventType) { + return AbstractPersistenceEvent.class.isAssignableFrom(eventType); + } + + /** + * @deprecated Replaced by {@link org.grails.datastore.gorm.events.AutoTimestampEventListener} + */ + @Deprecated + public TimestampProvider getTimestampProvider() { + return new DefaultTimestampProvider(); + } + + /** + * @deprecated Replaced by {@link org.grails.datastore.gorm.events.AutoTimestampEventListener} + */ + @Deprecated + public void setTimestampProvider(TimestampProvider timestampProvider) { + // no-op + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/CouldNotDetermineHibernateDialectException.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/CouldNotDetermineHibernateDialectException.java new file mode 100644 index 00000000000..40ecc9a2339 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/CouldNotDetermineHibernateDialectException.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.exceptions; + +/** + * Thrown when no Hibernate dialect could be found for a database name. + * + * @author Steven Devijver + */ +public class CouldNotDetermineHibernateDialectException extends GrailsHibernateException { + + private static final long serialVersionUID = -3385252525996110909L; + + public CouldNotDetermineHibernateDialectException(String message) { + super(message); + } + + public CouldNotDetermineHibernateDialectException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsHibernateConfigurationException.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsHibernateConfigurationException.java new file mode 100644 index 00000000000..95b514074ae --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsHibernateConfigurationException.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.exceptions; + +/** + * Thrown when configuration Hibernate for GORM features. + * + * @author Graeme Rocher + * @since 1.1 + */ +public class GrailsHibernateConfigurationException extends GrailsHibernateException { + + private static final long serialVersionUID = 5212907914995954558L; + + public GrailsHibernateConfigurationException(String message) { + super(message); + } + + public GrailsHibernateConfigurationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsHibernateException.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsHibernateException.java new file mode 100644 index 00000000000..959f3feaf05 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsHibernateException.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.exceptions; + +import org.grails.datastore.mapping.core.DatastoreException; + +/** + * Base exception class for errors related to Hibernate configuration in Grails. + * + * @author Steven Devijver + */ +public abstract class GrailsHibernateException extends DatastoreException { + + private static final long serialVersionUID = -6019220941440364736L; + + public GrailsHibernateException(String message) { + super(message); + } + + public GrailsHibernateException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsQueryException.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsQueryException.java new file mode 100644 index 00000000000..578439e8143 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/exceptions/GrailsQueryException.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.exceptions; + +import org.grails.datastore.mapping.core.DatastoreException; + +/** + * Base exception class for errors related to Domain class queries in Grails. + * + * @author Graeme Rocher + */ +public class GrailsQueryException extends DatastoreException { + + private static final long serialVersionUID = 775603608315415077L; + + public GrailsQueryException(String message, Throwable cause) { + super(message, cause); + } + + public GrailsQueryException(String message) { + super(message); + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.java new file mode 100644 index 00000000000..ffe1f054529 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.multitenancy; + +import java.io.Serializable; + +import org.springframework.context.ApplicationEvent; + +import grails.gorm.multitenancy.Tenants; +import org.grails.datastore.gorm.GormEnhancer; +import org.grails.datastore.mapping.core.Datastore; +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent; +import org.grails.datastore.mapping.engine.event.PersistenceEventListener; +import org.grails.datastore.mapping.engine.event.PreInsertEvent; +import org.grails.datastore.mapping.engine.event.PreUpdateEvent; +import org.grails.datastore.mapping.engine.event.ValidationEvent; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.types.TenantId; +import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore; +import org.grails.datastore.mapping.multitenancy.exceptions.TenantException; +import org.grails.datastore.mapping.query.Query; +import org.grails.datastore.mapping.query.event.PreQueryEvent; +import org.grails.orm.hibernate.AbstractHibernateDatastore; + +/** + * An event listener that hooks into persistence events to enable discriminator based multi tenancy (ie {@link org.grails.datastore.mapping.multitenancy.MultiTenancySettings.MultiTenancyMode#DISCRIMINATOR} + * + * @author Graeme Rocher + * @since 6.0 + */ +public class MultiTenantEventListener implements PersistenceEventListener { + @Override + public boolean supportsEventType(Class eventType) { + return org.grails.datastore.gorm.multitenancy.MultiTenantEventListener.SUPPORTED_EVENTS.contains(eventType); + } + + @Override + public boolean supportsSourceType(Class sourceType) { + return AbstractHibernateDatastore.class.isAssignableFrom(sourceType); + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (supportsEventType(event.getClass())) { + Datastore hibernateDatastore = (Datastore) event.getSource(); + if (event instanceof PreQueryEvent) { + PreQueryEvent preQueryEvent = (PreQueryEvent) event; + Query query = preQueryEvent.getQuery(); + + PersistentEntity entity = query.getEntity(); + if (entity.isMultiTenant()) { + if (hibernateDatastore == null) { + hibernateDatastore = GormEnhancer.findDatastore(entity.getJavaClass()); + } + if (supportsSourceType(hibernateDatastore.getClass())) { + ((AbstractHibernateDatastore) hibernateDatastore).enableMultiTenancyFilter(); + } + } + } + else if ((event instanceof ValidationEvent) || (event instanceof PreInsertEvent) || (event instanceof PreUpdateEvent)) { + AbstractPersistenceEvent preInsertEvent = (AbstractPersistenceEvent) event; + PersistentEntity entity = preInsertEvent.getEntity(); + if (entity.isMultiTenant()) { + TenantId tenantId = entity.getTenantId(); + if (hibernateDatastore == null) { + hibernateDatastore = GormEnhancer.findDatastore(entity.getJavaClass()); + } + if (supportsSourceType(hibernateDatastore.getClass())) { + Serializable currentId; + + if (hibernateDatastore instanceof MultiTenantCapableDatastore) { + currentId = Tenants.currentId((MultiTenantCapableDatastore) hibernateDatastore); + } + else { + currentId = Tenants.currentId(hibernateDatastore.getClass()); + } + if (currentId != null) { + try { + if (currentId == ConnectionSource.DEFAULT) { + currentId = (Serializable) preInsertEvent.getEntityAccess().getProperty(tenantId.getName()); + } + preInsertEvent.getEntityAccess().setProperty(tenantId.getName(), currentId); + } catch (Exception e) { + throw new TenantException("Could not assigned tenant id [" + currentId + "] to property [" + tenantId + "], probably due to a type mismatch. You should return a type from the tenant resolver that matches the property type of the tenant id!: " + e.getMessage(), e); + } + } + } + } + } + } + } + + @Override + public int getOrder() { + return DEFAULT_ORDER; + } +} + diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java new file mode 100644 index 00000000000..f5f771b2b35 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.proxy; + +import java.io.Serializable; + +import org.hibernate.Hibernate; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.proxy.HibernateProxy; +import org.hibernate.proxy.HibernateProxyHelper; + +import org.grails.datastore.mapping.core.Session; +import org.grails.datastore.mapping.engine.AssociationQueryExecutor; +import org.grails.datastore.mapping.proxy.ProxyFactory; +import org.grails.datastore.mapping.proxy.ProxyHandler; +import org.grails.datastore.mapping.reflect.ClassPropertyFetcher; + +/** + * Implementation of the ProxyHandler interface for Hibernate using org.hibernate.Hibernate + * and HibernateProxyHelper where possible. + * + * @author Graeme Rocher + * @since 1.2.2 + */ +public class HibernateProxyHandler implements ProxyHandler, ProxyFactory { + + /** + * Check if the proxy or persistent collection is initialized. + * {@inheritDoc} + */ + @Override + public boolean isInitialized(Object o) { + return Hibernate.isInitialized(o); + } + + /** + * Check if an association proxy or persistent collection is initialized. + * {@inheritDoc} + */ + @Override + public boolean isInitialized(Object obj, String associationName) { + try { + Object proxy = ClassPropertyFetcher.getInstancePropertyValue(obj, associationName); + return isInitialized(proxy); + } + catch (RuntimeException e) { + return false; + } + } + + /** + * Unproxies a HibernateProxy. If the proxy is uninitialized, it automatically triggers an initialization. + * In case the supplied object is null or not a proxy, the object will be returned as-is. + * {@inheritDoc} + * @see Hibernate#unproxy + */ + @Override + public Object unwrap(Object object) { + if (object instanceof PersistentCollection) { + initialize(object); + return object; + } + return Hibernate.unproxy(object); + } + + /** + * {@inheritDoc} + * @see org.hibernate.proxy.AbstractLazyInitializer#getIdentifier + */ + @Override + public Serializable getIdentifier(Object o) { + if (o instanceof HibernateProxy) { + return ((HibernateProxy) o).getHibernateLazyInitializer().getIdentifier(); + } + else { + //TODO seems we can get the id here if its has normal getId + // PersistentEntity persistentEntity = GormEnhancer.findStaticApi(o.getClass()).getGormPersistentEntity(); + // return persistentEntity.getMappingContext().getEntityReflector(persistentEntity).getIdentifier(o); + return null; + } + } + + /** + * {@inheritDoc} + * @see HibernateProxyHelper#getClassWithoutInitializingProxy + */ + @Override + public Class getProxiedClass(Object o) { + return HibernateProxyHelper.getClassWithoutInitializingProxy(o); + } + + /** + * calls unwrap which calls unproxy + * @see #unwrap(Object) + * @deprecated use unwrap + */ + @Deprecated + public Object unwrapIfProxy(Object instance) { + return unwrap(instance); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isProxy(Object o) { + return (o instanceof HibernateProxy) || (o instanceof PersistentCollection); + } + + /** + * Force initialization of a proxy or persistent collection. + * {@inheritDoc} + */ + @Override + public void initialize(Object o) { + Hibernate.initialize(o); + } + + @Override + public T createProxy(Session session, Class type, Serializable key) { + throw new UnsupportedOperationException("createProxy not supported in HibernateProxyHandler"); + } + + @Override + public T createProxy(Session session, AssociationQueryExecutor executor, K associationKey) { + throw new UnsupportedOperationException("createProxy not supported in HibernateProxyHandler"); + } + + /** + * @deprecated use unwrap + */ + @Deprecated + public Object unwrapProxy(Object proxy) { + return unwrap(proxy); + } + + /** + * returns the proxy for an association. returns null if its not a proxy. + * Note: Only used in a test. Deprecate? + */ + public HibernateProxy getAssociationProxy(Object obj, String associationName) { + try { + Object proxy = ClassPropertyFetcher.getInstancePropertyValue(obj, associationName); + if (proxy instanceof HibernateProxy) { + return (HibernateProxy) proxy; + } + return null; + } + catch (RuntimeException e) { + return null; + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandler.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandler.java new file mode 100644 index 00000000000..0adbe69b848 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandler.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.proxy; + +import java.io.Serializable; + +import groovy.lang.GroovyObject; +import groovy.lang.GroovySystem; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.proxy.HibernateProxy; +import org.hibernate.proxy.HibernateProxyHelper; +import org.hibernate.proxy.LazyInitializer; + +import org.grails.datastore.mapping.core.Session; +import org.grails.datastore.mapping.engine.AssociationQueryExecutor; +import org.grails.datastore.mapping.proxy.JavassistProxyFactory; +import org.grails.datastore.mapping.proxy.ProxyFactory; +import org.grails.datastore.mapping.proxy.ProxyHandler; +import org.grails.datastore.mapping.reflect.ClassPropertyFetcher; + +/** + * Implementation of the ProxyHandler interface for Hibernate. + * Deprecated as Hibernate 5.6+ no longer supports Javassist + * + * @author Graeme Rocher + * @since 1.2.2 + * @deprecated + */ + +@Deprecated +public class SimpleHibernateProxyHandler extends JavassistProxyFactory implements ProxyHandler, ProxyFactory { + + public boolean isInitialized(Object o) { + if (o instanceof HibernateProxy) { + return !((HibernateProxy) o).getHibernateLazyInitializer().isUninitialized(); + } + else if (o instanceof PersistentCollection) { + return ((PersistentCollection) o).wasInitialized(); + } + else { + return super.isInitialized(o); + } + } + + public boolean isInitialized(Object obj, String associationName) { + try { + Object proxy = ClassPropertyFetcher.getInstancePropertyValue(obj, associationName); + return isInitialized(proxy); + } + catch (RuntimeException e) { + return false; + } + } + + @Override + public Object unwrap(Object object) { + return unwrapIfProxy(object); + } + + @Override + public Serializable getIdentifier(Object obj) { + return (Serializable) getProxyIdentifier(obj); + } + + public Object unwrapIfProxy(Object instance) { + if (instance instanceof HibernateProxy) { + final HibernateProxy proxy = (HibernateProxy) instance; + return unwrapProxy(proxy); + } + else { + return super.unwrap(instance); + } + } + + public Object unwrapProxy(final HibernateProxy proxy) { + final LazyInitializer lazyInitializer = proxy.getHibernateLazyInitializer(); + if (lazyInitializer.isUninitialized()) { + lazyInitializer.initialize(); + } + final Object obj = lazyInitializer.getImplementation(); + if (obj != null) { + ensureCorrectGroovyMetaClass(obj, obj.getClass()); + } + return obj; + } + + /** + * Ensures the meta class is correct for a given class + * + * @param target The GroovyObject + * @param persistentClass The persistent class + */ + private static void ensureCorrectGroovyMetaClass(Object target, Class persistentClass) { + if (target instanceof GroovyObject) { + GroovyObject go = ((GroovyObject) target); + if (!go.getMetaClass().getTheClass().equals(persistentClass)) { + go.setMetaClass(GroovySystem.getMetaClassRegistry().getMetaClass(persistentClass)); + } + } + } + + public HibernateProxy getAssociationProxy(Object obj, String associationName) { + try { + Object proxy = ClassPropertyFetcher.getInstancePropertyValue(obj, associationName); + if (proxy instanceof HibernateProxy) { + return (HibernateProxy) proxy; + } + return null; + } + catch (RuntimeException e) { + return null; + } + } + + public boolean isProxy(Object o) { + return (o instanceof HibernateProxy) || super.isProxy(o); + } + + public void initialize(Object o) { + if (o instanceof HibernateProxy) { + final LazyInitializer hibernateLazyInitializer = ((HibernateProxy) o).getHibernateLazyInitializer(); + if (hibernateLazyInitializer.isUninitialized()) { + hibernateLazyInitializer.initialize(); + } + } + else { + super.initialize(o); + } + } + + public Object getProxyIdentifier(Object o) { + if (o instanceof HibernateProxy) { + return ((HibernateProxy) o).getHibernateLazyInitializer().getIdentifier(); + } + return super.getIdentifier(o); + } + + public Class getProxiedClass(Object o) { + if (o instanceof HibernateProxy) { + return HibernateProxyHelper.getClassWithoutInitializingProxy(o); + } + else { + return super.getProxiedClass(o); + } + } + + @Override + public T createProxy(Session session, Class type, Serializable key) { + return super.createProxy(session, type, key); + } + + @Override + public T createProxy(Session session, AssociationQueryExecutor executor, K associationKey) { + return super.createProxy(session, executor, associationKey); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java new file mode 100644 index 00000000000..750669538d2 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java @@ -0,0 +1,2195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.query; + +import java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import groovy.lang.Closure; +import groovy.lang.DelegatesTo; +import groovy.lang.GroovyObjectSupport; +import groovy.lang.MetaClass; +import groovy.lang.MetaMethod; +import groovy.lang.MissingMethodException; + +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.EntityType; + +import org.hibernate.Criteria; +import org.hibernate.FetchMode; +import org.hibernate.LockMode; +import org.hibernate.Metamodel; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.TypeHelper; +import org.hibernate.criterion.AggregateProjection; +import org.hibernate.criterion.CountProjection; +import org.hibernate.criterion.CriteriaSpecification; +import org.hibernate.criterion.Criterion; +import org.hibernate.criterion.IdentifierProjection; +import org.hibernate.criterion.Junction; +import org.hibernate.criterion.Order; +import org.hibernate.criterion.Projection; +import org.hibernate.criterion.ProjectionList; +import org.hibernate.criterion.Projections; +import org.hibernate.criterion.Property; +import org.hibernate.criterion.PropertyProjection; +import org.hibernate.criterion.Restrictions; +import org.hibernate.criterion.SimpleExpression; +import org.hibernate.criterion.Subqueries; +import org.hibernate.transform.ResultTransformer; +import org.hibernate.type.Type; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.convert.ConversionService; + +import grails.gorm.MultiTenant; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.types.Basic; +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings; +import org.grails.datastore.mapping.query.Query; +import org.grails.datastore.mapping.query.api.BuildableCriteria; +import org.grails.datastore.mapping.query.api.QueryableCriteria; +import org.grails.datastore.mapping.reflect.NameUtils; +import org.grails.orm.hibernate.AbstractHibernateDatastore; + +/** + * Abstract super class for sharing code between Hibernate 3 and 4 implementations of HibernateCriteriaBuilder + * + * @author Graeme Rocher + * @since 3.0.7 + */ +public abstract class AbstractHibernateCriteriaBuilder extends GroovyObjectSupport implements org.grails.datastore.mapping.query.api.BuildableCriteria, org.grails.datastore.mapping.query.api.ProjectionList { + + public static final String AND = "and"; // builder + public static final String IS_NULL = "isNull"; // builder + public static final String IS_NOT_NULL = "isNotNull"; // builder + public static final String NOT = "not"; // builder + public static final String OR = "or"; // builder + public static final String ID_EQUALS = "idEq"; // builder + public static final String IS_EMPTY = "isEmpty"; //builder + public static final String IS_NOT_EMPTY = "isNotEmpty"; //builder + public static final String RLIKE = "rlike"; //method + public static final String BETWEEN = "between"; //method + public static final String EQUALS = "eq"; //method + public static final String EQUALS_PROPERTY = "eqProperty"; //method + public static final String GREATER_THAN = "gt"; //method + public static final String GREATER_THAN_PROPERTY = "gtProperty"; //method + public static final String GREATER_THAN_OR_EQUAL = "ge"; //method + public static final String GREATER_THAN_OR_EQUAL_PROPERTY = "geProperty"; //method + public static final String ILIKE = "ilike"; //method + public static final String IN = "in"; //method + public static final String LESS_THAN = "lt"; //method + public static final String LESS_THAN_PROPERTY = "ltProperty"; //method + public static final String LESS_THAN_OR_EQUAL = "le"; //method + public static final String LESS_THAN_OR_EQUAL_PROPERTY = "leProperty"; //method + public static final String LIKE = "like"; //method + public static final String NOT_EQUAL = "ne"; //method + public static final String NOT_EQUAL_PROPERTY = "neProperty"; //method + public static final String SIZE_EQUALS = "sizeEq"; //method + public static final String ORDER_DESCENDING = "desc"; + public static final String ORDER_ASCENDING = "asc"; + protected static final String ROOT_DO_CALL = "doCall"; + protected static final String ROOT_CALL = "call"; + protected static final String LIST_CALL = "list"; + protected static final String LIST_DISTINCT_CALL = "listDistinct"; + protected static final String COUNT_CALL = "count"; + protected static final String GET_CALL = "get"; + protected static final String SCROLL_CALL = "scroll"; + protected static final String SET_RESULT_TRANSFORMER_CALL = "setResultTransformer"; + protected static final String PROJECTIONS = "projections"; + + protected SessionFactory sessionFactory; + protected Session hibernateSession; + protected Class targetClass; + protected Criteria criteria; + protected MetaClass criteriaMetaClass; + protected boolean uniqueResult = false; + protected List logicalExpressionStack = new ArrayList<>(); + protected List associationStack = new ArrayList<>(); + protected boolean participate; + protected boolean scroll; + protected boolean count; + protected ProjectionList projectionList = Projections.projectionList(); + protected List aliasStack = new ArrayList<>(); + protected List aliasInstanceStack = new ArrayList<>(); + protected Map aliasMap = new HashMap<>(); + protected static final String ALIAS = "_alias"; + protected ResultTransformer resultTransformer; + protected int aliasCount; + protected boolean paginationEnabledList = false; + protected List orderEntries; + protected ConversionService conversionService; + protected int defaultFlushMode; + protected AbstractHibernateDatastore datastore; + + @SuppressWarnings("rawtypes") + public AbstractHibernateCriteriaBuilder(Class targetClass, SessionFactory sessionFactory) { + this.targetClass = targetClass; + this.sessionFactory = sessionFactory; + } + + @SuppressWarnings("rawtypes") + public AbstractHibernateCriteriaBuilder(Class targetClass, SessionFactory sessionFactory, boolean uniqueResult) { + this.targetClass = targetClass; + this.sessionFactory = sessionFactory; + this.uniqueResult = uniqueResult; + } + + public void setDatastore(AbstractHibernateDatastore datastore) { + this.datastore = datastore; + if (MultiTenant.class.isAssignableFrom(targetClass) && datastore.getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR) { + datastore.enableMultiTenancyFilter(); + } + } + + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * A projection that selects a property name + * @param propertyName The name of the property + */ + public org.grails.datastore.mapping.query.api.ProjectionList property(String propertyName) { + return property(propertyName, null); + } + + /** + * A projection that selects a property name + * @param propertyName The name of the property + * @param alias The alias to use + */ + public org.grails.datastore.mapping.query.api.ProjectionList property(String propertyName, String alias) { + final PropertyProjection propertyProjection = Projections.property(calculatePropertyName(propertyName)); + addProjectionToList(propertyProjection, alias); + return this; + } + + /** + * Adds a projection to the projectList for the given alias + * + * @param propertyProjection The projection + * @param alias The alias + */ + protected void addProjectionToList(Projection propertyProjection, String alias) { + if (alias != null) { + projectionList.add(propertyProjection, alias); + } + else { + projectionList.add(propertyProjection); + } + } + + /** + * Adds a sql projection to the criteria + * + * @param sql SQL projecting a single value + * @param columnAlias column alias for the projected value + * @param type the type of the projected value + */ + protected void sqlProjection(String sql, String columnAlias, Type type) { + sqlProjection(sql, Collections.singletonList(columnAlias), Collections.singletonList(type)); + } + + /** + * Adds a sql projection to the criteria + * + * @param sql SQL projecting + * @param columnAliases List of column aliases for the projected values + * @param types List of types for the projected values + */ + protected void sqlProjection(String sql, List columnAliases, List types) { + projectionList.add(Projections.sqlProjection(sql, columnAliases.toArray(new String[columnAliases.size()]), types.toArray(new Type[types.size()]))); + } + + /** + * Adds a sql projection to the criteria + * + * @param sql SQL projecting + * @param groupBy group by clause + * @param columnAliases List of column aliases for the projected values + * @param types List of types for the projected values + */ + protected void sqlGroupProjection(String sql, String groupBy, List columnAliases, List types) { + projectionList.add(Projections.sqlGroupProjection(sql, groupBy, columnAliases.toArray(new String[columnAliases.size()]), types.toArray(new Type[types.size()]))); + } + + /** + * A projection that selects a distince property name + * @param propertyName The property name + */ + public org.grails.datastore.mapping.query.api.ProjectionList distinct(String propertyName) { + distinct(propertyName, null); + return this; + } + + /** + * A projection that selects a distince property name + * @param propertyName The property name + * @param alias The alias to use + */ + public org.grails.datastore.mapping.query.api.ProjectionList distinct(String propertyName, String alias) { + final Projection proj = Projections.distinct(Projections.property(calculatePropertyName(propertyName))); + addProjectionToList(proj, alias); + return this; + } + + /** + * A distinct projection that takes a list + * + * @param propertyNames The list of distince property names + */ + @SuppressWarnings("rawtypes") + public org.grails.datastore.mapping.query.api.ProjectionList distinct(Collection propertyNames) { + return distinct(propertyNames, null); + } + + /** + * A distinct projection that takes a list + * + * @param propertyNames The list of distince property names + * @param alias The alias to use + */ + @SuppressWarnings("rawtypes") + public org.grails.datastore.mapping.query.api.ProjectionList distinct(Collection propertyNames, String alias) { + ProjectionList list = Projections.projectionList(); + for (Object o : propertyNames) { + list.add(Projections.property(calculatePropertyName(o.toString()))); + } + final Projection proj = Projections.distinct(list); + addProjectionToList(proj, alias); + return this; + } + + /** + * Adds a projection that allows the criteria to return the property average value + * + * @param propertyName The name of the property + */ + public org.grails.datastore.mapping.query.api.ProjectionList avg(String propertyName) { + return avg(propertyName, null); + } + + /** + * Adds a projection that allows the criteria to return the property average value + * + * @param propertyName The name of the property + * @param alias The alias to use + */ + public org.grails.datastore.mapping.query.api.ProjectionList avg(String propertyName, String alias) { + final AggregateProjection aggregateProjection = Projections.avg(calculatePropertyName(propertyName)); + addProjectionToList(aggregateProjection, alias); + return this; + } + + /** + * Use a join query + * + * @param associationPath The path of the association + */ + public BuildableCriteria join(String associationPath) { + criteria.setFetchMode(calculatePropertyName(associationPath), FetchMode.JOIN); + return this; + } + + public BuildableCriteria join(String property, JoinType joinType) { + criteria.setFetchMode(calculatePropertyName(property), FetchMode.JOIN); + return this; + } + + /** + * Whether a pessimistic lock should be obtained. + * + * @param shouldLock True if it should + */ + public void lock(boolean shouldLock) { + String lastAlias = getLastAlias(); + + if (shouldLock) { + if (lastAlias != null) { + criteria.setLockMode(lastAlias, LockMode.PESSIMISTIC_WRITE); + } + else { + criteria.setLockMode(LockMode.PESSIMISTIC_WRITE); + } + } + else { + if (lastAlias != null) { + criteria.setLockMode(lastAlias, LockMode.NONE); + } + else { + criteria.setLockMode(LockMode.NONE); + } + } + } + + /** + * Use a select query + * + * @param associationPath The path of the association + */ + public BuildableCriteria select(String associationPath) { + criteria.setFetchMode(calculatePropertyName(associationPath), FetchMode.SELECT); + return this; + } + + /** + * Whether to use the query cache + * @param shouldCache True if the query should be cached + */ + public BuildableCriteria cache(boolean shouldCache) { + criteria.setCacheable(shouldCache); + return this; + } + + /** + * Whether to check for changes on the objects loaded + * @param readOnly True to disable dirty checking + */ + public BuildableCriteria readOnly(boolean readOnly) { + criteria.setReadOnly(readOnly); + return this; + } + + /** + * Calculates the property name including any alias paths + * + * @param propertyName The property name + * @return The calculated property name + */ + protected String calculatePropertyName(String propertyName) { + String lastAlias = getLastAlias(); + if (lastAlias != null) { + return lastAlias + '.' + propertyName; + } + + return propertyName; + } + + private String getLastAlias() { + if (aliasStack.size() > 0) { + return aliasStack.get(aliasStack.size() - 1).toString(); + } + return null; + } + + public Class getTargetClass() { + return targetClass; + } + + /** + * Calculates the property value, converting GStrings if necessary + * + * @param propertyValue The property value + * @return The calculated property value + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected Object calculatePropertyValue(Object propertyValue) { + if (propertyValue instanceof CharSequence) { + return propertyValue.toString(); + } + if (propertyValue instanceof QueryableCriteria) { + propertyValue = convertToHibernateCriteria((QueryableCriteria) propertyValue); + } + else if (propertyValue instanceof Closure) { + propertyValue = convertToHibernateCriteria( + new grails.gorm.DetachedCriteria(targetClass).build((Closure) propertyValue)); + } + return propertyValue; + } + + protected abstract org.hibernate.criterion.DetachedCriteria convertToHibernateCriteria(QueryableCriteria queryableCriteria); + + /** + * Adds a projection that allows the criteria to return the property count + * + * @param propertyName The name of the property + */ + public void count(String propertyName) { + count(propertyName, null); + } + + /** + * Adds a projection that allows the criteria to return the property count + * + * @param propertyName The name of the property + * @param alias The alias to use + */ + public void count(String propertyName, String alias) { + final CountProjection proj = Projections.count(calculatePropertyName(propertyName)); + addProjectionToList(proj, alias); + } + + public org.grails.datastore.mapping.query.api.ProjectionList id() { + final IdentifierProjection proj = Projections.id(); + addProjectionToList(proj, null); + return this; + } + + public org.grails.datastore.mapping.query.api.ProjectionList count() { + return rowCount(); + } + + /** + * Adds a projection that allows the criteria to return the distinct property count + * + * @param propertyName The name of the property + */ + public org.grails.datastore.mapping.query.api.ProjectionList countDistinct(String propertyName) { + return countDistinct(propertyName, null); + } + + /** + * Adds a projection that allows the criteria to return the distinct property count + * + * @param propertyName The name of the property + */ + public org.grails.datastore.mapping.query.api.ProjectionList groupProperty(String propertyName) { + groupProperty(propertyName, null); + return this; + } + + public org.grails.datastore.mapping.query.api.ProjectionList distinct() { + criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); + return this; + } + + /** + * Adds a projection that allows the criteria to return the distinct property count + * + * @param propertyName The name of the property + * @param alias The alias to use + */ + public org.grails.datastore.mapping.query.api.ProjectionList countDistinct(String propertyName, String alias) { + final CountProjection proj = Projections.countDistinct(calculatePropertyName(propertyName)); + addProjectionToList(proj, alias); + return this; + } + + /** + * Adds a projection that allows the criteria's result to be grouped by a property + * + * @param propertyName The name of the property + * @param alias The alias to use + */ + public org.grails.datastore.mapping.query.api.ProjectionList groupProperty(String propertyName, String alias) { + final PropertyProjection proj = Projections.groupProperty(calculatePropertyName(propertyName)); + addProjectionToList(proj, alias); + return this; + } + + /** + * Adds a projection that allows the criteria to retrieve a maximum property value + * + * @param propertyName The name of the property + */ + public org.grails.datastore.mapping.query.api.ProjectionList max(String propertyName) { + return max(propertyName, null); + } + + /** + * Adds a projection that allows the criteria to retrieve a maximum property value + * + * @param propertyName The name of the property + * @param alias The alias to use + */ + public org.grails.datastore.mapping.query.api.ProjectionList max(String propertyName, String alias) { + final AggregateProjection proj = Projections.max(calculatePropertyName(propertyName)); + addProjectionToList(proj, alias); + return this; + } + + /** + * Adds a projection that allows the criteria to retrieve a minimum property value + * + * @param propertyName The name of the property + */ + public org.grails.datastore.mapping.query.api.ProjectionList min(String propertyName) { + return min(propertyName, null); + } + + /** + * Adds a projection that allows the criteria to retrieve a minimum property value + * + * @param alias The alias to use + */ + public org.grails.datastore.mapping.query.api.ProjectionList min(String propertyName, String alias) { + final AggregateProjection aggregateProjection = Projections.min(calculatePropertyName(propertyName)); + addProjectionToList(aggregateProjection, alias); + return this; + } + + /** + * Adds a projection that allows the criteria to return the row count + * + */ + public org.grails.datastore.mapping.query.api.ProjectionList rowCount() { + return rowCount(null); + } + + /** + * Adds a projection that allows the criteria to return the row count + * + * @param alias The alias to use + */ + public org.grails.datastore.mapping.query.api.ProjectionList rowCount(String alias) { + final Projection proj = Projections.rowCount(); + addProjectionToList(proj, alias); + return this; + } + + /** + * Adds a projection that allows the criteria to retrieve the sum of the results of a property + * + * @param propertyName The name of the property + */ + public org.grails.datastore.mapping.query.api.ProjectionList sum(String propertyName) { + return sum(propertyName, null); + } + + /** + * Adds a projection that allows the criteria to retrieve the sum of the results of a property + * + * @param propertyName The name of the property + * @param alias The alias to use + */ + public org.grails.datastore.mapping.query.api.ProjectionList sum(String propertyName, String alias) { + final AggregateProjection proj = Projections.sum(calculatePropertyName(propertyName)); + addProjectionToList(proj, alias); + return this; + } + + /** + * Sets the fetch mode of an associated path + * + * @param associationPath The name of the associated path + * @param fetchMode The fetch mode to set + */ + public void fetchMode(String associationPath, FetchMode fetchMode) { + if (criteria != null) { + criteria.setFetchMode(associationPath, fetchMode); + } + } + + /** + * Sets the resultTransformer. + * @param transformer The result transformer to use. + */ + public void resultTransformer(ResultTransformer transformer) { + if (criteria == null) { + throwRuntimeException(new IllegalArgumentException("Call to [resultTransformer] not supported here")); + } + resultTransformer = transformer; + } + + /** + * Join an association, assigning an alias to the joined association. + * + * Functionally equivalent to createAlias(String, String, int) using + * CriteriaSpecificationINNER_JOIN for the joinType. + * + * @param associationPath A dot-seperated property path + * @param alias The alias to assign to the joined association (for later reference). + * + * @return this (for method chaining) + * #see {@link #createAlias(String, String, int)} + * @throws org.hibernate.HibernateException Indicates a problem creating the sub criteria + */ + public Criteria createAlias(String associationPath, String alias) { + aliasMap.put(associationPath, alias); + return criteria.createAlias(associationPath, alias); + } + + /** + * Creates a Criterion that compares to class properties for equality + * @param propertyName The first property name + * @param otherPropertyName The second property name + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria eqProperty(String propertyName, String otherPropertyName) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [eqProperty] with propertyName [" + + propertyName + "] and other property name [" + otherPropertyName + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + otherPropertyName = calculatePropertyName(otherPropertyName); + addToCriteria(Restrictions.eqProperty(propertyName, otherPropertyName)); + return this; + } + + /** + * Creates a Criterion that compares to class properties for !equality + * @param propertyName The first property name + * @param otherPropertyName The second property name + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria neProperty(String propertyName, String otherPropertyName) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [neProperty] with propertyName [" + + propertyName + "] and other property name [" + otherPropertyName + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + otherPropertyName = calculatePropertyName(otherPropertyName); + addToCriteria(Restrictions.neProperty(propertyName, otherPropertyName)); + return this; + } + + /** + * Creates a Criterion that tests if the first property is greater than the second property + * @param propertyName The first property name + * @param otherPropertyName The second property name + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria gtProperty(String propertyName, String otherPropertyName) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [gtProperty] with propertyName [" + + propertyName + "] and other property name [" + otherPropertyName + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + otherPropertyName = calculatePropertyName(otherPropertyName); + addToCriteria(Restrictions.gtProperty(propertyName, otherPropertyName)); + return this; + } + + /** + * Creates a Criterion that tests if the first property is greater than or equal to the second property + * @param propertyName The first property name + * @param otherPropertyName The second property name + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria geProperty(String propertyName, String otherPropertyName) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [geProperty] with propertyName [" + + propertyName + "] and other property name [" + otherPropertyName + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + otherPropertyName = calculatePropertyName(otherPropertyName); + addToCriteria(Restrictions.geProperty(propertyName, otherPropertyName)); + return this; + } + + /** + * Creates a Criterion that tests if the first property is less than the second property + * @param propertyName The first property name + * @param otherPropertyName The second property name + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria ltProperty(String propertyName, String otherPropertyName) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [ltProperty] with propertyName [" + + propertyName + "] and other property name [" + otherPropertyName + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + otherPropertyName = calculatePropertyName(otherPropertyName); + addToCriteria(Restrictions.ltProperty(propertyName, otherPropertyName)); + return this; + } + + /** + * Creates a Criterion that tests if the first property is less than or equal to the second property + * @param propertyName The first property name + * @param otherPropertyName The second property name + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria leProperty(String propertyName, String otherPropertyName) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [leProperty] with propertyName [" + + propertyName + "] and other property name [" + otherPropertyName + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + otherPropertyName = calculatePropertyName(otherPropertyName); + addToCriteria(Restrictions.leProperty(propertyName, otherPropertyName)); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria allEq(Map propertyValues) { + addToCriteria(Restrictions.allEq(propertyValues)); + return this; + } + + /** + * Creates a subquery criterion that ensures the given property is equal to all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public org.grails.datastore.mapping.query.api.Criteria eqAll(String propertyName, Closure propertyValue) { + return eqAll(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(propertyValue)); + } + + /** + * Creates a subquery criterion that ensures the given property is greater than all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public org.grails.datastore.mapping.query.api.Criteria gtAll(String propertyName, Closure propertyValue) { + return gtAll(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(propertyValue)); + } + + /** + * Creates a subquery criterion that ensures the given property is less than all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public org.grails.datastore.mapping.query.api.Criteria ltAll(String propertyName, Closure propertyValue) { + return ltAll(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(propertyValue)); + } + + /** + * Creates a subquery criterion that ensures the given property is greater than all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public org.grails.datastore.mapping.query.api.Criteria geAll(String propertyName, Closure propertyValue) { + return geAll(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(propertyValue)); + } + + /** + * Creates a subquery criterion that ensures the given property is less than all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public org.grails.datastore.mapping.query.api.Criteria leAll(String propertyName, Closure propertyValue) { + return leAll(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(propertyValue)); + } + + /** + * Creates a subquery criterion that ensures the given property is equal to all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria eqAll(String propertyName, + @SuppressWarnings("rawtypes") QueryableCriteria propertyValue) { + addToCriteria(Property.forName(propertyName).eqAll(convertToHibernateCriteria(propertyValue))); + return this; + } + + /** + * Creates a subquery criterion that ensures the given property is greater than all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria gtAll(String propertyName, + @SuppressWarnings("rawtypes") QueryableCriteria propertyValue) { + addToCriteria(Property.forName(propertyName).gtAll(convertToHibernateCriteria(propertyValue))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria gtSome(String propertyName, QueryableCriteria propertyValue) { + addToCriteria(Property.forName(propertyName).gtSome(convertToHibernateCriteria(propertyValue))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria gtSome(String propertyName, Closure propertyValue) { + return gtSome(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(propertyValue)); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria geSome(String propertyName, QueryableCriteria propertyValue) { + addToCriteria(Property.forName(propertyName).geSome(convertToHibernateCriteria(propertyValue))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria geSome(String propertyName, Closure propertyValue) { + return geSome(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(propertyValue)); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria ltSome(String propertyName, QueryableCriteria propertyValue) { + addToCriteria(Property.forName(propertyName).ltSome(convertToHibernateCriteria(propertyValue))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria ltSome(String propertyName, Closure propertyValue) { + return ltSome(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(propertyValue)); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria leSome(String propertyName, QueryableCriteria propertyValue) { + addToCriteria(Property.forName(propertyName).leSome(convertToHibernateCriteria(propertyValue))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria leSome(String propertyName, Closure propertyValue) { + return leSome(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(propertyValue)); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria in(String propertyName, QueryableCriteria subquery) { + return inList(propertyName, subquery); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria inList(String propertyName, QueryableCriteria subquery) { + addToCriteria(Property.forName(propertyName).in(convertToHibernateCriteria(subquery))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria in(String propertyName, Closure subquery) { + return inList(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(subquery)); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria inList(String propertyName, Closure subquery) { + return inList(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(subquery)); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria notIn(String propertyName, QueryableCriteria subquery) { + addToCriteria(Property.forName(propertyName).notIn(convertToHibernateCriteria(subquery))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria notIn(String propertyName, Closure subquery) { + return notIn(propertyName, new grails.gorm.DetachedCriteria(targetClass).build(subquery)); + } + + /** + * Creates a subquery criterion that ensures the given property is less than all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria ltAll(String propertyName, + @SuppressWarnings("rawtypes") QueryableCriteria propertyValue) { + addToCriteria(Property.forName(propertyName).ltAll(convertToHibernateCriteria(propertyValue))); + return this; + + } + + /** + * Creates a subquery criterion that ensures the given property is greater than all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria geAll(String propertyName, + @SuppressWarnings("rawtypes") QueryableCriteria propertyValue) { + addToCriteria(Property.forName(propertyName).geAll(convertToHibernateCriteria(propertyValue))); + return this; + + } + + /** + * Creates a subquery criterion that ensures the given property is less than all the given returned values + * + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria leAll(String propertyName, + @SuppressWarnings("rawtypes") QueryableCriteria propertyValue) { + addToCriteria(Property.forName(propertyName).leAll(convertToHibernateCriteria(propertyValue))); + return this; + + } + + /** + * Creates a "greater than" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria gt(String propertyName, Object propertyValue) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [gt] with propertyName [" + + propertyName + "] and value [" + propertyValue + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + propertyValue = calculatePropertyValue(propertyValue); + + Criterion gt; + if (propertyValue instanceof org.hibernate.criterion.DetachedCriteria) { + gt = Property.forName(propertyName).gt((org.hibernate.criterion.DetachedCriteria) propertyValue); + } + else { + gt = Restrictions.gt(propertyName, propertyValue); + } + addToCriteria(gt); + return this; + } + + public org.grails.datastore.mapping.query.api.Criteria lte(String s, Object o) { + return le(s, o); + } + + /** + * Creates a "greater than or equal to" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria ge(String propertyName, Object propertyValue) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [ge] with propertyName [" + + propertyName + "] and value [" + propertyValue + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + propertyValue = calculatePropertyValue(propertyValue); + + Criterion ge; + if (propertyValue instanceof org.hibernate.criterion.DetachedCriteria) { + ge = Property.forName(propertyName).ge((org.hibernate.criterion.DetachedCriteria) propertyValue); + } + else { + ge = Restrictions.ge(propertyName, propertyValue); + } + addToCriteria(ge); + return this; + } + + /** + * Creates a "less than" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria lt(String propertyName, Object propertyValue) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [lt] with propertyName [" + + propertyName + "] and value [" + propertyValue + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + propertyValue = calculatePropertyValue(propertyValue); + Criterion lt; + if (propertyValue instanceof org.hibernate.criterion.DetachedCriteria) { + lt = Property.forName(propertyName).lt((org.hibernate.criterion.DetachedCriteria) propertyValue); + } + else { + lt = Restrictions.lt(propertyName, propertyValue); + } + addToCriteria(lt); + return this; + } + + /** + * Creates a "less than or equal to" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria le(String propertyName, Object propertyValue) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [le] with propertyName [" + + propertyName + "] and value [" + propertyValue + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + propertyValue = calculatePropertyValue(propertyValue); + Criterion le; + if (propertyValue instanceof org.hibernate.criterion.DetachedCriteria) { + le = Property.forName(propertyName).le((org.hibernate.criterion.DetachedCriteria) propertyValue); + } + else { + le = Restrictions.le(propertyName, propertyValue); + } + addToCriteria(le); + return this; + } + + public org.grails.datastore.mapping.query.api.Criteria idEquals(Object o) { + return idEq(o); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria exists(QueryableCriteria subquery) { + addToCriteria(Subqueries.exists(convertToHibernateCriteria(subquery))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria notExists(QueryableCriteria subquery) { + addToCriteria(Subqueries.notExists(convertToHibernateCriteria(subquery))); + return this; + } + + public org.grails.datastore.mapping.query.api.Criteria isEmpty(String property) { + String propertyName = calculatePropertyName(property); + addToCriteria(Restrictions.isEmpty(propertyName)); + return this; + } + + public org.grails.datastore.mapping.query.api.Criteria isNotEmpty(String property) { + String propertyName = calculatePropertyName(property); + addToCriteria(Restrictions.isNotEmpty(propertyName)); + return this; + } + + public org.grails.datastore.mapping.query.api.Criteria isNull(String property) { + String propertyName = calculatePropertyName(property); + addToCriteria(Restrictions.isNull(propertyName)); + return this; + } + + public org.grails.datastore.mapping.query.api.Criteria isNotNull(String property) { + String propertyName = calculatePropertyName(property); + addToCriteria(Restrictions.isNotNull(propertyName)); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria and(Closure callable) { + return executeLogicalExpression(callable, AND); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria or(Closure callable) { + return executeLogicalExpression(callable, OR); + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria not(Closure callable) { + return executeLogicalExpression(callable, NOT); + } + + protected org.grails.datastore.mapping.query.api.Criteria executeLogicalExpression(Closure callable, String logicalOperator) { + logicalExpressionStack.add(new LogicalExpression(logicalOperator)); + try { + invokeClosureNode(callable); + } finally { + LogicalExpression logicalExpression = logicalExpressionStack.remove(logicalExpressionStack.size() - 1); + if (logicalExpression != null) + addToCriteria(logicalExpression.toCriterion()); + } + + return this; + } + + /** + * Creates an "equals" Criterion based on the specified property name and value. Case-sensitive. + * @param propertyName The property name + * @param propertyValue The property value + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria eq(String propertyName, Object propertyValue) { + return eq(propertyName, propertyValue, Collections.emptyMap()); + } + + public org.grails.datastore.mapping.query.api.Criteria idEq(Object o) { + return eq("id", o); + } + + /** + * Groovy moves the map to the first parameter if using the idiomatic form, e.g. + * eq 'firstName', 'Fred', ignoreCase: true. + * @param params optional map with customization parameters; currently only 'ignoreCase' is supported. + * @param propertyName + * @param propertyValue + * @return A Criterion instance + */ + @SuppressWarnings("rawtypes") + public org.grails.datastore.mapping.query.api.Criteria eq(Map params, String propertyName, Object propertyValue) { + return eq(propertyName, propertyValue, params); + } + + /** + * Creates an "equals" Criterion based on the specified property name and value. + * Supports case-insensitive search if the params map contains true + * under the 'ignoreCase' key. + * @param propertyName The property name + * @param propertyValue The property value + * @param params optional map with customization parameters; currently only 'ignoreCase' is supported. + * + * @return A Criterion instance + */ + @SuppressWarnings("rawtypes") + public org.grails.datastore.mapping.query.api.Criteria eq(String propertyName, Object propertyValue, Map params) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [eq] with propertyName [" + + propertyName + "] and value [" + propertyValue + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + propertyValue = calculatePropertyValue(propertyValue); + Criterion eq; + if (propertyValue instanceof org.hibernate.criterion.DetachedCriteria) { + eq = Property.forName(propertyName).eq((org.hibernate.criterion.DetachedCriteria) propertyValue); + } + else { + eq = Restrictions.eq(propertyName, propertyValue); + } + if (params != null && (eq instanceof SimpleExpression)) { + Object ignoreCase = params.get("ignoreCase"); + if (ignoreCase instanceof Boolean && (Boolean) ignoreCase) { + eq = ((SimpleExpression) eq).ignoreCase(); + } + } + addToCriteria(eq); + return this; + } + + /** + * Applies a sql restriction to the results to allow something like: + * + * @param sqlRestriction the sql restriction + * @return a Criteria instance + */ + public org.grails.datastore.mapping.query.api.Criteria sqlRestriction(String sqlRestriction) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [sqlRestriction] with value [" + + sqlRestriction + "] not allowed here.")); + } + return sqlRestriction(sqlRestriction, Collections.EMPTY_LIST); + } + + /** + * Applies a sql restriction to the results to allow something like: + * + * @param sqlRestriction the sql restriction + * @param values jdbc parameters + * @return a Criteria instance + */ + public org.grails.datastore.mapping.query.api.Criteria sqlRestriction(String sqlRestriction, List values) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [sqlRestriction] with value [" + + sqlRestriction + "] not allowed here.")); + } + final int numberOfParameters = values.size(); + + final Type[] typesArray = new Type[numberOfParameters]; + final Object[] valuesArray = new Object[numberOfParameters]; + + if (numberOfParameters > 0) { + final TypeHelper typeHelper = sessionFactory.getTypeHelper(); + for (int i = 0; i < typesArray.length; i++) { + final Object value = values.get(i); + typesArray[i] = typeHelper.basic(value.getClass()); + valuesArray[i] = value; + } + } + addToCriteria(Restrictions.sqlRestriction(sqlRestriction, valuesArray, typesArray)); + return this; + } + + /** + * Creates a Criterion with from the specified property name and "like" expression + * @param propertyName The property name + * @param propertyValue The like value + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria like(String propertyName, Object propertyValue) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [like] with propertyName [" + + propertyName + "] and value [" + propertyValue + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + propertyValue = calculatePropertyValue(propertyValue); + addToCriteria(Restrictions.like(propertyName, propertyValue)); + return this; + } + + /** + * Creates a Criterion with from the specified property name and "rlike" (a regular expression version of "like") expression + * @param propertyName The property name + * @param propertyValue The ilike value + * + * @return A Criterion instance + */ + public abstract org.grails.datastore.mapping.query.api.Criteria rlike(String propertyName, Object propertyValue); + + /** + * Creates a Criterion with from the specified property name and "ilike" (a case sensitive version of "like") expression + * @param propertyName The property name + * @param propertyValue The ilike value + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria ilike(String propertyName, Object propertyValue) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [ilike] with propertyName [" + + propertyName + "] and value [" + propertyValue + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + propertyValue = calculatePropertyValue(propertyValue); + addToCriteria(Restrictions.ilike(propertyName, propertyValue)); + return this; + } + + /** + * Applys a "in" contrain on the specified property + * @param propertyName The property name + * @param values A collection of values + * + * @return A Criterion instance + */ + @SuppressWarnings("rawtypes") + public org.grails.datastore.mapping.query.api.Criteria in(String propertyName, Collection values) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [in] with propertyName [" + + propertyName + "] and values [" + values + "] not allowed here.")); + } + + // Preserve the original property name before alias prefix is applied, + // since isBasicCollectionProperty needs the raw property name for entity lookup. + String originalPropertyName = propertyName; + propertyName = calculatePropertyName(propertyName); + + if (values instanceof List) { + values = convertArgumentList((List) values); + } + + // Handle basic collection types (hasMany to String/Integer/etc.) + // These are stored in a separate join table and cannot use simple Restrictions.in(). + // Instead, create an alias to the collection table and restrict on 'elements'. + if (isBasicCollectionProperty(originalPropertyName)) { + String alias; + if (aliasMap.containsKey(propertyName)) { + alias = aliasMap.get(propertyName); + } else { + alias = propertyName + ALIAS; + createAlias(propertyName, alias); + aliasMap.put(propertyName, alias); + } + addToCriteria(Restrictions.in(alias + ".elements", values == null ? Collections.EMPTY_LIST : values)); + } else { + addToCriteria(Restrictions.in(propertyName, values == null ? Collections.EMPTY_LIST : values)); + } + return this; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected List convertArgumentList(List argList) { + List convertedList = new ArrayList(argList.size()); + for (Object item : argList) { + if (item instanceof CharSequence) { + item = item.toString(); + } + convertedList.add(item); + } + return convertedList; + } + + /** + * Delegates to in as in is a Groovy keyword + */ + @SuppressWarnings("rawtypes") + public org.grails.datastore.mapping.query.api.Criteria inList(String propertyName, Collection values) { + return in(propertyName, values); + } + + /** + * Delegates to in as in is a Groovy keyword + */ + public org.grails.datastore.mapping.query.api.Criteria inList(String propertyName, Object[] values) { + return in(propertyName, values); + } + + /** + * Applys a "in" contrain on the specified property + * @param propertyName The property name + * @param values A collection of values + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria in(String propertyName, Object[] values) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [in] with propertyName [" + + propertyName + "] and values [" + values + "] not allowed here.")); + } + + // Preserve the original property name before alias prefix is applied, + // since isBasicCollectionProperty needs the raw property name for entity lookup. + String originalPropertyName = propertyName; + propertyName = calculatePropertyName(propertyName); + + // Handle basic collection types (hasMany to String/Integer/etc.) + if (isBasicCollectionProperty(originalPropertyName)) { + String alias; + if (aliasMap.containsKey(propertyName)) { + alias = aliasMap.get(propertyName); + } else { + alias = propertyName + ALIAS; + createAlias(propertyName, alias); + aliasMap.put(propertyName, alias); + } + addToCriteria(Restrictions.in(alias + ".elements", values)); + } else { + addToCriteria(Restrictions.in(propertyName, values)); + } + return this; + } + + /** + * Orders by the specified property name (defaults to ascending) + * + * @param propertyName The property name to order by + * @return A Order instance + */ + public org.grails.datastore.mapping.query.api.Criteria order(String propertyName) { + if (criteria == null) { + throwRuntimeException(new IllegalArgumentException("Call to [order] with propertyName [" + + propertyName + "]not allowed here.")); + } + propertyName = calculatePropertyName(propertyName); + Order o = Order.asc(propertyName); + addOrderInternal(this.criteria, o); + return this; + } + + /** + * Orders by the specified property name (defaults to ascending) + * + * @param o The property name to order by + * @return A Order instance + */ + public org.grails.datastore.mapping.query.api.Criteria order(Order o) { + final Criteria criteria = this.criteria; + addOrderInternal(criteria, o); + return this; + } + + private void addOrderInternal(Criteria criteria, Order o) { + if (criteria == null) { + throwRuntimeException(new IllegalArgumentException("Call to [order] not allowed here.")); + } + if (paginationEnabledList) { + orderEntries.add(o); + } + else { + criteria.addOrder(o); + } + } + + @Override + public org.grails.datastore.mapping.query.api.Criteria order(Query.Order o) { + + final Criteria criteria = this.criteria; + final String property = o.getProperty(); + addOrderInternal(criteria, o, property); + return this; + } + + private void addOrderInternal(Criteria criteria, Query.Order o, String property) { + final int i = property.indexOf('.'); + if (i == -1) { + + Order order = convertOrder(o, property); + addOrderInternal(criteria, order); + } + else { + String sortHead = property.substring(0, i); + String sortTail = property.substring(i + 1); + createAliasIfNeccessary(sortHead, sortHead, org.hibernate.sql.JoinType.INNER_JOIN.getJoinTypeValue()); + final Criteria sub = aliasInstanceStack.get(aliasInstanceStack.size() - 1); + addOrderInternal(sub, o, sortTail); + } + } + + protected Order convertOrder(Query.Order o, String property) { + Order order; + switch (o.getDirection()) { + case DESC: + order = Order.desc(property); + break; + default: + order = Order.asc(property); + break; + } + if (o.isIgnoreCase()) { + order.ignoreCase(); + } + return order; + } + + /** + * Orders by the specified property name and direction + * + * @param propertyName The property name to order by + * @param direction Either "asc" for ascending or "desc" for descending + * + * @return A Order instance + */ + public org.grails.datastore.mapping.query.api.Criteria order(String propertyName, String direction) { + if (criteria == null) { + throwRuntimeException(new IllegalArgumentException("Call to [order] with propertyName [" + + propertyName + "]not allowed here.")); + } + propertyName = calculatePropertyName(propertyName); + Order o; + if (direction.equals(ORDER_DESCENDING)) { + o = Order.desc(propertyName); + } + else { + o = Order.asc(propertyName); + } + if (paginationEnabledList) { + orderEntries.add(o); + } + else { + criteria.addOrder(o); + } + return this; + } + + /** + * Creates a Criterion that contrains a collection property by size + * + * @param propertyName The property name + * @param size The size to constrain by + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria sizeEq(String propertyName, int size) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [sizeEq] with propertyName [" + + propertyName + "] and size [" + size + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + addToCriteria(Restrictions.sizeEq(propertyName, size)); + return this; + } + + /** + * Creates a Criterion that contrains a collection property to be greater than the given size + * + * @param propertyName The property name + * @param size The size to constrain by + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria sizeGt(String propertyName, int size) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [sizeGt] with propertyName [" + + propertyName + "] and size [" + size + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + addToCriteria(Restrictions.sizeGt(propertyName, size)); + return this; + } + + /** + * Creates a Criterion that contrains a collection property to be greater than or equal to the given size + * + * @param propertyName The property name + * @param size The size to constrain by + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria sizeGe(String propertyName, int size) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [sizeGe] with propertyName [" + + propertyName + "] and size [" + size + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + addToCriteria(Restrictions.sizeGe(propertyName, size)); + return this; + } + + /** + * Creates a Criterion that contrains a collection property to be less than or equal to the given size + * + * @param propertyName The property name + * @param size The size to constrain by + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria sizeLe(String propertyName, int size) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [sizeLe] with propertyName [" + + propertyName + "] and size [" + size + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + addToCriteria(Restrictions.sizeLe(propertyName, size)); + return this; + } + + /** + * Creates a Criterion that contrains a collection property to be less than to the given size + * + * @param propertyName The property name + * @param size The size to constrain by + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria sizeLt(String propertyName, int size) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [sizeLt] with propertyName [" + + propertyName + "] and size [" + size + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + addToCriteria(Restrictions.sizeLt(propertyName, size)); + return this; + } + + /** + * Creates a Criterion that contrains a collection property to be not equal to the given size + * + * @param propertyName The property name + * @param size The size to constrain by + * + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria sizeNe(String propertyName, int size) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [sizeNe] with propertyName [" + + propertyName + "] and size [" + size + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + addToCriteria(Restrictions.sizeNe(propertyName, size)); + return this; + } + + /** + * Creates a "not equal" Criterion based on the specified property name and value + * @param propertyName The property name + * @param propertyValue The property value + * @return The criterion object + */ + public org.grails.datastore.mapping.query.api.Criteria ne(String propertyName, Object propertyValue) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [ne] with propertyName [" + + propertyName + "] and value [" + propertyValue + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + propertyValue = calculatePropertyValue(propertyValue); + addToCriteria(Restrictions.ne(propertyName, propertyValue)); + return this; + } + + public org.grails.datastore.mapping.query.api.Criteria notEqual(String propertyName, Object propertyValue) { + return ne(propertyName, propertyValue); + } + + /** + * Creates a "between" Criterion based on the property name and specified lo and hi values + * @param propertyName The property name + * @param lo The low value + * @param hi The high value + * @return A Criterion instance + */ + public org.grails.datastore.mapping.query.api.Criteria between(String propertyName, Object lo, Object hi) { + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [between] with propertyName [" + + propertyName + "] not allowed here.")); + } + + propertyName = calculatePropertyName(propertyName); + addToCriteria(Restrictions.between(propertyName, lo, hi)); + return this; + } + + public org.grails.datastore.mapping.query.api.Criteria gte(String s, Object o) { + return ge(s, o); + } + + protected boolean validateSimpleExpression() { + return criteria != null; + } + + @Override + public Object list(@DelegatesTo(Criteria.class) Closure c) { + return invokeMethod(LIST_CALL, new Object[]{c}); + } + + @Override + public Object list(Map params, @DelegatesTo(Criteria.class) Closure c) { + return invokeMethod(LIST_CALL, new Object[]{params, c}); + } + + @Override + public Object listDistinct(@DelegatesTo(Criteria.class) Closure c) { + return invokeMethod(LIST_DISTINCT_CALL, new Object[]{c}); + } + + @Override + public Object get(@DelegatesTo(Criteria.class) Closure c) { + return invokeMethod(GET_CALL, new Object[]{c}); + } + + @Override + public Object scroll(@DelegatesTo(Criteria.class) Closure c) { + return invokeMethod(SCROLL_CALL, new Object[]{c}); + } + + @SuppressWarnings("rawtypes") + @Override + public Object invokeMethod(String name, Object obj) { + Object[] args = obj.getClass().isArray() ? (Object[]) obj : new Object[]{obj}; + + if (paginationEnabledList && SET_RESULT_TRANSFORMER_CALL.equals(name) && args.length == 1 && + args[0] instanceof ResultTransformer) { + resultTransformer = (ResultTransformer) args[0]; + return null; + } + + if (isCriteriaConstructionMethod(name, args)) { + if (criteria != null) { + throwRuntimeException(new IllegalArgumentException("call to [" + name + "] not supported here")); + } + + if (name.equals(GET_CALL)) { + uniqueResult = true; + } + else if (name.equals(SCROLL_CALL)) { + scroll = true; + } + else if (name.equals(COUNT_CALL)) { + count = true; + } + else if (name.equals(LIST_DISTINCT_CALL)) { + resultTransformer = CriteriaSpecification.DISTINCT_ROOT_ENTITY; + } + + createCriteriaInstance(); + + // Check for pagination params + if (name.equals(LIST_CALL) && args.length == 2) { + paginationEnabledList = true; + orderEntries = new ArrayList<>(); + invokeClosureNode(args[1]); + } + else { + invokeClosureNode(args[0]); + } + + if (resultTransformer != null) { + criteria.setResultTransformer(resultTransformer); + } + Object result; + if (!uniqueResult) { + if (scroll) { + result = criteria.scroll(); + } + else if (count) { + criteria.setProjection(Projections.rowCount()); + result = criteria.uniqueResult(); + } + else if (paginationEnabledList) { + // Calculate how many results there are in total. This has been + // moved to before the 'list()' invocation to avoid any "ORDER + // BY" clause added by 'populateArgumentsForCriteria()', otherwise + // an exception is thrown for non-string sort fields (GRAILS-2690). + criteria.setFirstResult(0); + criteria.setMaxResults(Integer.MAX_VALUE); + + // Restore the previous projection, add settings for the pagination parameters, + // and then execute the query. + boolean isProjection = (projectionList != null && projectionList.getLength() > 0); + criteria.setProjection(isProjection ? projectionList : null); + + for (Order orderEntry : orderEntries) { + criteria.addOrder(orderEntry); + } + if (resultTransformer == null) { + // GRAILS-9644 - Use projection transformer + criteria.setResultTransformer(isProjection ? + CriteriaSpecification.PROJECTION : + CriteriaSpecification.ROOT_ENTITY + ); + } + else if (paginationEnabledList) { + // relevant to GRAILS-5692 + criteria.setResultTransformer(resultTransformer); + } + // GRAILS-7324 look if we already have association to sort by + Map argMap = (Map) args[0]; + final String sort = (String) argMap.get(HibernateQueryConstants.ARGUMENT_SORT); + if (sort != null) { + boolean ignoreCase = true; + Object caseArg = argMap.get(HibernateQueryConstants.ARGUMENT_IGNORE_CASE); + if (caseArg instanceof Boolean) { + ignoreCase = (Boolean) caseArg; + } + final String orderParam = (String) argMap.get(HibernateQueryConstants.ARGUMENT_ORDER); + final String order = HibernateQueryConstants.ORDER_DESC.equalsIgnoreCase(orderParam) ? + HibernateQueryConstants.ORDER_DESC : HibernateQueryConstants.ORDER_ASC; + int lastPropertyPos = sort.lastIndexOf('.'); + String associationForOrdering = lastPropertyPos >= 0 ? sort.substring(0, lastPropertyPos) : null; + if (associationForOrdering != null && aliasMap.containsKey(associationForOrdering)) { + addOrder(criteria, aliasMap.get(associationForOrdering) + "." + sort.substring(lastPropertyPos + 1), + order, ignoreCase); + // remove sort from arguments map to exclude from default processing. + @SuppressWarnings("unchecked") Map argMap2 = new HashMap(argMap); + argMap2.remove(HibernateQueryConstants.ARGUMENT_SORT); + argMap = argMap2; + } + } + result = createPagedResultList(argMap); + } + else { + result = criteria.list(); + } + } + else { + result = executeUniqueResultWithProxyUnwrap(); + } + if (!participate) { + closeSession(); + } + return result; + } + + if (criteria == null) createCriteriaInstance(); + + MetaMethod metaMethod = getMetaClass().getMetaMethod(name, args); + if (metaMethod != null) { + return metaMethod.invoke(this, args); + } + + metaMethod = criteriaMetaClass.getMetaMethod(name, args); + if (metaMethod != null) { + return metaMethod.invoke(criteria, args); + } + metaMethod = criteriaMetaClass.getMetaMethod(NameUtils.getSetterName(name), args); + if (metaMethod != null) { + return metaMethod.invoke(criteria, args); + } + + if (isAssociationQueryMethod(args) || isAssociationQueryWithJoinSpecificationMethod(args)) { + final boolean hasMoreThanOneArg = args.length > 1; + Object callable = hasMoreThanOneArg ? args[1] : args[0]; + int joinType = hasMoreThanOneArg ? (Integer) args[0] : org.hibernate.sql.JoinType.INNER_JOIN.getJoinTypeValue(); + + if (name.equals(AND) || name.equals(OR) || name.equals(NOT)) { + if (criteria == null) { + throwRuntimeException(new IllegalArgumentException("call to [" + name + "] not supported here")); + } + + logicalExpressionStack.add(new LogicalExpression(name)); + invokeClosureNode(callable); + + LogicalExpression logicalExpression = logicalExpressionStack.remove(logicalExpressionStack.size() - 1); + addToCriteria(logicalExpression.toCriterion()); + + return name; + } + + if (name.equals(PROJECTIONS) && args.length == 1 && (args[0] instanceof Closure)) { + if (criteria == null) { + throwRuntimeException(new IllegalArgumentException("call to [" + name + "] not supported here")); + } + + projectionList = Projections.projectionList(); + invokeClosureNode(callable); + + if (projectionList != null && projectionList.getLength() > 0) { + criteria.setProjection(projectionList); + } + + return name; + } + + final PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(targetClass, name); + if (pd != null && pd.getReadMethod() != null) { + final Metamodel metamodel = sessionFactory.getMetamodel(); + final EntityType entityType = metamodel.entity(targetClass); + + Attribute attribute = null; + try { + attribute = entityType.getAttribute(name); + } catch (IllegalArgumentException e) { + // Composite ID components may not be registered as JPA Metamodel + // attributes. Fall back to checking if the property type is a managed + // entity so the criteria builder can navigate associations that form + // part of a composite key (e.g. UserRole with composite: ['user', 'role']). + Class propertyType = pd.getPropertyType(); + try { + metamodel.entity(propertyType); + // Property type is a managed entity - treat as association + Class oldTargetClass = targetClass; + targetClass = propertyType; + if (targetClass.equals(oldTargetClass) && !hasMoreThanOneArg) { + joinType = org.hibernate.sql.JoinType.LEFT_OUTER_JOIN.getJoinTypeValue(); + } + associationStack.add(name); + final String associationPath = getAssociationPath(); + createAliasIfNeccessary(name, associationPath, joinType); + logicalExpressionStack.add(new LogicalExpression(AND)); + invokeClosureNode(callable); + aliasStack.remove(aliasStack.size() - 1); + if (!aliasInstanceStack.isEmpty()) { + aliasInstanceStack.remove(aliasInstanceStack.size() - 1); + } + LogicalExpression logicalExpression = logicalExpressionStack.remove(logicalExpressionStack.size() - 1); + if (!logicalExpression.args.isEmpty()) { + addToCriteria(logicalExpression.toCriterion()); + } + associationStack.remove(associationStack.size() - 1); + targetClass = oldTargetClass; + return name; + } catch (IllegalArgumentException ignored) { + // Not a managed entity type - wrap the original exception to preserve stack trace + throw new IllegalArgumentException( + "Unable to locate attribute [" + name + "] on entity [" + entityType.getName() + "]", e); + } + } + + if (attribute.isAssociation()) { + Class oldTargetClass = targetClass; + targetClass = getClassForAssociationType(attribute); + if (targetClass.equals(oldTargetClass) && !hasMoreThanOneArg) { + joinType = org.hibernate.sql.JoinType.LEFT_OUTER_JOIN.getJoinTypeValue(); // default to left join if joining on the same table + } + associationStack.add(name); + final String associationPath = getAssociationPath(); + createAliasIfNeccessary(name, associationPath, joinType); + // the criteria within an association node are grouped with an implicit AND + logicalExpressionStack.add(new LogicalExpression(AND)); + invokeClosureNode(callable); + aliasStack.remove(aliasStack.size() - 1); + if (!aliasInstanceStack.isEmpty()) { + aliasInstanceStack.remove(aliasInstanceStack.size() - 1); + } + LogicalExpression logicalExpression = logicalExpressionStack.remove(logicalExpressionStack.size() - 1); + if (!logicalExpression.args.isEmpty()) { + addToCriteria(logicalExpression.toCriterion()); + } + associationStack.remove(associationStack.size() - 1); + targetClass = oldTargetClass; + + return name; + } + if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED) { + associationStack.add(name); + logicalExpressionStack.add(new LogicalExpression(AND)); + Class oldTargetClass = targetClass; + targetClass = pd.getPropertyType(); + invokeClosureNode(callable); + targetClass = oldTargetClass; + LogicalExpression logicalExpression = logicalExpressionStack.remove(logicalExpressionStack.size() - 1); + if (!logicalExpression.args.isEmpty()) { + addToCriteria(logicalExpression.toCriterion()); + } + associationStack.remove(associationStack.size() - 1); + return name; + } + } + } + else if (args.length == 1 && args[0] != null) { + if (criteria == null) { + throwRuntimeException(new IllegalArgumentException("call to [" + name + "] not supported here")); + } + + Object value = args[0]; + Criterion c = null; + if (name.equals(ID_EQUALS)) { + return eq("id", value); + } + + if (name.equals(IS_NULL) || + name.equals(IS_NOT_NULL) || + name.equals(IS_EMPTY) || + name.equals(IS_NOT_EMPTY)) { + if (!(value instanceof String)) { + throwRuntimeException(new IllegalArgumentException("call to [" + name + "] with value [" + + value + "] requires a String value.")); + } + String propertyName = calculatePropertyName((String) value); + if (name.equals(IS_NULL)) { + c = Restrictions.isNull(propertyName); + } + else if (name.equals(IS_NOT_NULL)) { + c = Restrictions.isNotNull(propertyName); + } + else if (name.equals(IS_EMPTY)) { + c = Restrictions.isEmpty(propertyName); + } + else if (name.equals(IS_NOT_EMPTY)) { + c = Restrictions.isNotEmpty(propertyName); + } + } + + if (c != null) { + return addToCriteria(c); + } + } + + throw new MissingMethodException(name, getClass(), args); + } + + protected abstract Object executeUniqueResultWithProxyUnwrap(); + + protected abstract List createPagedResultList(Map args); + + private boolean isAssociationQueryMethod(Object[] args) { + return args.length == 1 && args[0] instanceof Closure; + } + + private boolean isAssociationQueryWithJoinSpecificationMethod(Object[] args) { + return args.length == 2 && (args[0] instanceof Number) && (args[1] instanceof Closure); + } + + private void createAliasIfNeccessary(String associationName, String associationPath, int joinType) { + String newAlias; + if (aliasMap.containsKey(associationPath)) { + newAlias = aliasMap.get(associationPath); + } + else { + aliasCount++; + newAlias = associationName + ALIAS + aliasCount; + aliasMap.put(associationPath, newAlias); + aliasInstanceStack.add(createAlias(associationPath, newAlias, joinType)); + } + aliasStack.add(newAlias); + } + + private String getAssociationPath() { + StringBuilder fullPath = new StringBuilder(); + for (Object anAssociationStack : associationStack) { + String propertyName = (String) anAssociationStack; + if (fullPath.length() > 0) fullPath.append("."); + fullPath.append(propertyName); + } + return fullPath.toString(); + } + + private boolean isCriteriaConstructionMethod(String name, Object[] args) { + return (name.equals(LIST_CALL) && args.length == 2 && args[0] instanceof Map && args[1] instanceof Closure) || + (name.equals(ROOT_CALL) || + name.equals(ROOT_DO_CALL) || + name.equals(LIST_CALL) || + name.equals(LIST_DISTINCT_CALL) || + name.equals(GET_CALL) || + name.equals(COUNT_CALL) || + name.equals(SCROLL_CALL) && args.length == 1 && args[0] instanceof Closure); + } + + public Criteria buildCriteria(Closure criteriaClosure) { + createCriteriaInstance(); + criteriaClosure.setDelegate(this); + criteriaClosure.call(); + return criteria; + } + + protected abstract void createCriteriaInstance(); + + protected abstract void cacheCriteriaMapping(); + + private void invokeClosureNode(Object args) { + Closure callable = (Closure) args; + callable.setDelegate(this); + callable.setResolveStrategy(Closure.DELEGATE_FIRST); + callable.call(); + } + + /** + * adds and returns the given criterion to the currently active criteria set. + * this might be either the root criteria or a currently open + * LogicalExpression. + */ + protected Criterion addToCriteria(Criterion c) { + if (!logicalExpressionStack.isEmpty()) { + logicalExpressionStack.get(logicalExpressionStack.size() - 1).args.add(c); + } + else { + criteria.add(c); + } + return c; + } + + /** + * Add order directly to criteria. + */ + private static void addOrder(Criteria c, String sort, String order, boolean ignoreCase) { + if (HibernateQueryConstants.ORDER_DESC.equals(order)) { + c.addOrder(ignoreCase ? Order.desc(sort).ignoreCase() : Order.desc(sort)); + } + else { + c.addOrder(ignoreCase ? Order.asc(sort).ignoreCase() : Order.asc(sort)); + } + } + + /** + * Checks if the given property name refers to a Basic collection type + * (e.g. hasMany: [schools: String]). Basic collections are stored in + * a separate join table and require special handling for 'in' queries. + */ + private boolean isBasicCollectionProperty(String propertyName) { + if (datastore == null) { + return false; + } + PersistentEntity entity = datastore.getMappingContext().getPersistentEntity(targetClass.getName()); + if (entity == null) { + return false; + } + PersistentProperty property = entity.getPropertyByName(propertyName); + return property instanceof Basic; + } + + /** + * Returns the criteria instance + * @return The criteria instance + */ + public Criteria getInstance() { + return criteria; + } + + /** + * Set whether a unique result should be returned + * @param uniqueResult True if a unique result should be returned + */ + public void setUniqueResult(boolean uniqueResult) { + this.uniqueResult = uniqueResult; + } + + /** + * Join an association using the specified join-type, assigning an alias + * to the joined association. + * sub + * The joinType is expected to be one of CriteriaSpecification.INNER_JOIN (the default), + * CriteriaSpecificationFULL_JOIN, or CriteriaSpecificationLEFT_JOIN. + * + * @param associationPath A dot-seperated property path + * @param alias The alias to assign to the joined association (for later reference). + * @param joinType The type of join to use. + * + * @return this (for method chaining) + * @throws org.hibernate.HibernateException Indicates a problem creating the sub criteria + */ + public abstract Criteria createAlias(String associationPath, String alias, int joinType); + + protected abstract Class getClassForAssociationType(Attribute type); + + /** + * instances of this class are pushed onto the logicalExpressionStack + * to represent all the unfinished "and", "or", and "not" expressions. + */ + protected class LogicalExpression { + public final Object name; + public final List args = new ArrayList<>(); + + public LogicalExpression(Object name) { + this.name = name; + } + + public Criterion toCriterion() { + if (name.equals(NOT)) { + switch (args.size()) { + case 0: + throwRuntimeException(new IllegalArgumentException("Logical expression [not] must contain at least 1 expression")); + return null; + + case 1: + return Restrictions.not(args.get(0)); + + default: + // treat multiple sub-criteria as an implicit "OR" + return Restrictions.not(buildJunction(Restrictions.disjunction(), args)); + } + } + + if (name.equals(AND)) { + return buildJunction(Restrictions.conjunction(), args); + } + + if (name.equals(OR)) { + return buildJunction(Restrictions.disjunction(), args); + } + + throwRuntimeException(new IllegalStateException("Logical expression [" + name + "] not handled!")); + return null; + } + + // add the Criterion objects in the given list to the given junction. + public Junction buildJunction(Junction junction, List criterions) { + for (Criterion c : criterions) { + junction.add(c); + } + + return junction; + } + } + + /** + * Throws a runtime exception where necessary to ensure the session gets closed + */ + protected void throwRuntimeException(RuntimeException t) { + closeSessionFollowingException(); + throw t; + } + + private void closeSessionFollowingException() { + closeSession(); + criteria = null; + } + + /** + * Closes the session if it is copen + */ + protected void closeSession() { + if (hibernateSession != null && hibernateSession.isOpen() && !participate) { + hibernateSession.close(); + } + } + + public int getDefaultFlushMode() { + return defaultFlushMode; + } + + public void setDefaultFlushMode(int defaultFlushMode) { + this.defaultFlushMode = defaultFlushMode; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriterionAdapter.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriterionAdapter.java new file mode 100644 index 00000000000..37f682d20e2 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriterionAdapter.java @@ -0,0 +1,570 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.query; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.criterion.Conjunction; +import org.hibernate.criterion.Criterion; +import org.hibernate.criterion.DetachedCriteria; +import org.hibernate.criterion.Disjunction; +import org.hibernate.criterion.Junction; +import org.hibernate.criterion.Property; +import org.hibernate.criterion.Restrictions; +import org.hibernate.criterion.Subqueries; + +import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.query.AssociationQuery; +import org.grails.datastore.mapping.query.Query; +import org.grails.datastore.mapping.query.api.QueryableCriteria; +import org.grails.datastore.mapping.query.criteria.FunctionCallingCriterion; + +/** + * Adapts Grails datastore API to Hibernate API + * + * @author Graeme Rocher + * @since 2.0 + */ +public abstract class AbstractHibernateCriterionAdapter { + protected static final Map, CriterionAdaptor> criterionAdaptors = new HashMap<>(); + protected static boolean initialized; + protected static final String ALIAS = "_alias"; + + public AbstractHibernateCriterionAdapter() { + initialize(); + } + + protected void initialize() { + if (initialized) { + return; + } + + synchronized (criterionAdaptors) { + // add simple property criterions (idEq, eq, ne, gt, lt, ge, le) + addSimplePropertyCriterionAdapters(); + + // add like operators (rlike, like, ilike) + addLikeCriterionAdapters(); + + //add simple size criterions (sizeEq, sizeGt, sizeLt, sizeGe, sizeLe) + addSizeComparisonCriterionAdapters(); + + //add simple criterions (isNull, isNotNull, isEmpty, isNotEmpty) + addSimpleCriterionAdapters(); + + //add simple property comparison criterions (eqProperty, neProperty, gtProperty, geProperty, ltProperty, leProperty) + addPropertyComparisonCriterionAdapters(); + + // add range queries (in, between) + addRangeQueryCriterionAdapters(); + + // add subquery adapters (gtAll, geAll, gtSome, ltAll, leAll) + addSubqueryCriterionAdapters(); + + // add junctions (conjunction, disjunction, negation) + addJunctionCriterionAdapters(); + + // add association query adapters + addAssociationQueryCriterionAdapters(); + } + + initialized = true; + } + + protected void addSubqueryCriterionAdapters() { + criterionAdaptors.put(Query.GreaterThanAll.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.GreaterThanAll criterion, String alias) { + QueryableCriteria subQuery = criterion.getValue(); + String propertyName = getPropertyName(criterion, alias); + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, subQuery); + return Property.forName(propertyName).gtAll(detachedCriteria); + } + }); + + criterionAdaptors.put(Query.GreaterThanEqualsAll.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.GreaterThanEqualsAll criterion, String alias) { + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, criterion.getValue()); + return Property.forName(getPropertyName(criterion, alias)).geAll(detachedCriteria); + } + }); + criterionAdaptors.put(Query.LessThanAll.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.LessThanAll criterion, String alias) { + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, criterion.getValue()); + return Property.forName(getPropertyName(criterion, alias)).ltAll(detachedCriteria); + } + }); + criterionAdaptors.put(Query.LessThanEqualsAll.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.LessThanEqualsAll criterion, String alias) { + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, criterion.getValue()); + return Property.forName(getPropertyName(criterion, alias)).leAll(detachedCriteria); + } + }); + + criterionAdaptors.put(Query.GreaterThanSome.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.GreaterThanSome criterion, String alias) { + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, criterion.getValue()); + return Property.forName(getPropertyName(criterion, alias)).gtSome(detachedCriteria); + } + }); + criterionAdaptors.put(Query.GreaterThanEqualsSome.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.GreaterThanEqualsSome criterion, String alias) { + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, criterion.getValue()); + return Property.forName(getPropertyName(criterion, alias)).geSome(detachedCriteria); + } + }); + criterionAdaptors.put(Query.LessThanSome.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.LessThanSome criterion, String alias) { + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, criterion.getValue()); + return Property.forName(getPropertyName(criterion, alias)).ltSome(detachedCriteria); + } + }); + criterionAdaptors.put(Query.LessThanEqualsSome.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.LessThanEqualsSome criterion, String alias) { + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, criterion.getValue()); + return Property.forName(getPropertyName(criterion, alias)).leSome(detachedCriteria); + } + }); + + criterionAdaptors.put(Query.NotIn.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.NotIn criterion, String alias) { + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, criterion.getSubquery()); + return Property.forName(getPropertyName(criterion, alias)).notIn(detachedCriteria); + } + }); + + criterionAdaptors.put(Query.Exists.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Exists criterion, String alias) { + final QueryableCriteria subquery = criterion.getSubquery(); + String subqueryAlias = subquery.getAlias(); + if (subquery.getAlias() == null) { + subqueryAlias = criterion.getSubquery().getPersistentEntity().getJavaClass().getSimpleName() + ALIAS; + } + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, subquery, subqueryAlias); + return Subqueries.exists(detachedCriteria); + } + }); + + criterionAdaptors.put(Query.NotExists.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.NotExists criterion, String alias) { + DetachedCriteria detachedCriteria = toHibernateDetachedCriteria(hibernateQuery, criterion.getSubquery()); + return Subqueries.notExists(detachedCriteria); + } + }); + } + + protected void addAssociationQueryCriterionAdapters() { + criterionAdaptors.put(DetachedAssociationCriteria.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) { + DetachedAssociationCriteria existing = (DetachedAssociationCriteria) criterion; + if (existing.getAlias() == null) { + alias = hibernateQuery.handleAssociationQuery(existing.getAssociation(), existing.getCriteria()); + } + else { + alias = hibernateQuery.handleAssociationQuery(existing.getAssociation(), existing.getCriteria(), existing.getAlias()); + } + Association association = existing.getAssociation(); + hibernateQuery.associationStack.add(association); + Junction conjunction = Restrictions.conjunction(); + try { + applySubCriteriaToJunction(association.getAssociatedEntity(), hibernateQuery, existing.getCriteria(), conjunction, alias); + return conjunction; + } finally { + hibernateQuery.associationStack.removeLast(); + } + } + }); + criterionAdaptors.put(AssociationQuery.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) { + AssociationQuery existing = (AssociationQuery) criterion; + Junction conjunction = Restrictions.conjunction(); + String newAlias = hibernateQuery.handleAssociationQuery(existing.getAssociation(), existing.getCriteria().getCriteria()); + if (alias == null) { + alias = newAlias; + } + else { + alias += '.' + newAlias; + } + applySubCriteriaToJunction(existing.getAssociation().getAssociatedEntity(), hibernateQuery, existing.getCriteria().getCriteria(), conjunction, alias); + return conjunction; + } + }); + } + + protected void addJunctionCriterionAdapters() { + criterionAdaptors.put(Query.Conjunction.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Conjunction criterion, String alias) { + Conjunction conjunction = Restrictions.conjunction(); + applySubCriteriaToJunction(hibernateQuery.getEntity(), hibernateQuery, criterion.getCriteria(), conjunction, alias); + return conjunction; + } + }); + criterionAdaptors.put(Query.Disjunction.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Disjunction criterion, String alias) { + Disjunction disjunction = Restrictions.disjunction(); + applySubCriteriaToJunction(hibernateQuery.getEntity(), hibernateQuery, criterion.getCriteria(), disjunction, alias); + return disjunction; + } + }); + criterionAdaptors.put(Query.Negation.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Negation criterion, String alias) { + CriterionAdaptor adapter = (CriterionAdaptor) criterionAdaptors.get(Query.Disjunction.class); + return Restrictions.not(adapter.toHibernateCriterion(hibernateQuery, new Query.Disjunction(criterion.getCriteria()), alias)); + } + }); + } + + protected void addRangeQueryCriterionAdapters() { + criterionAdaptors.put(Query.Between.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) { + Query.Between btwCriterion = (Query.Between) criterion; + return Restrictions.between(calculatePropertyName(btwCriterion.getProperty(), alias), btwCriterion.getFrom(), btwCriterion.getTo()); + } + }); + + criterionAdaptors.put(Query.In.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) { + Query.In inListQuery = (Query.In) criterion; + QueryableCriteria subquery = inListQuery.getSubquery(); + if (subquery != null) { + return Property.forName(getPropertyName(criterion, alias)).in(toHibernateDetachedCriteria(hibernateQuery, subquery)); + } + else { + return Restrictions.in(getPropertyName(criterion, alias), inListQuery.getValues()); + } + } + }); + } + + protected void addLikeCriterionAdapters() { + criterionAdaptors.put(Query.RLike.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) { + return createRlikeExpression(getPropertyName(criterion, alias), ((Query.RLike) criterion).getPattern()); + } + }); + criterionAdaptors.put(Query.Like.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Like criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + return Restrictions.like(propertyName, value); + } + }); + criterionAdaptors.put(Query.ILike.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.ILike criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + return Restrictions.ilike(propertyName, value); + } + }); + } + + protected void addPropertyComparisonCriterionAdapters() { + criterionAdaptors.put(Query.EqualsProperty.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.EqualsProperty criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.eqProperty(propertyName, criterion.getOtherProperty()); + } + }); + criterionAdaptors.put(Query.GreaterThanProperty.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.GreaterThanProperty criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.gtProperty(propertyName, criterion.getOtherProperty()); + } + }); + criterionAdaptors.put(Query.GreaterThanEqualsProperty.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.GreaterThanEqualsProperty criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.geProperty(propertyName, criterion.getOtherProperty()); + } + }); + criterionAdaptors.put(Query.LessThanProperty.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.LessThanProperty criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.ltProperty(propertyName, criterion.getOtherProperty()); + } + }); + criterionAdaptors.put(Query.LessThanEqualsProperty.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.LessThanEqualsProperty criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.leProperty(propertyName, criterion.getOtherProperty()); + } + }); + criterionAdaptors.put(Query.NotEqualsProperty.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.NotEqualsProperty criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.neProperty(propertyName, criterion.getOtherProperty()); + } + }); + } + + protected void addSimpleCriterionAdapters() { + criterionAdaptors.put(Query.IsNull.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.IsNull criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.isNull(propertyName); + } + }); + criterionAdaptors.put(Query.IsNotNull.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.IsNotNull criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.isNotNull(propertyName); + } + }); + criterionAdaptors.put(Query.IsEmpty.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.IsEmpty criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.isEmpty(propertyName); + } + }); + criterionAdaptors.put(Query.IsNotEmpty.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.IsNotEmpty criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + return Restrictions.isNotEmpty(propertyName); + } + }); + } + + protected void addSizeComparisonCriterionAdapters() { + criterionAdaptors.put(Query.SizeEquals.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.SizeEquals criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + int size = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); + return Restrictions.sizeEq(propertyName, size); + } + }); + + criterionAdaptors.put(Query.SizeGreaterThan.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.SizeGreaterThan criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + int size = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); + return Restrictions.sizeGt(propertyName, size); + } + }); + + criterionAdaptors.put(Query.SizeGreaterThanEquals.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.SizeGreaterThanEquals criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + int size = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); + return Restrictions.sizeGe(propertyName, size); + } + }); + + criterionAdaptors.put(Query.SizeLessThan.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.SizeLessThan criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + int size = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); + return Restrictions.sizeLt(propertyName, size); + } + }); + + criterionAdaptors.put(Query.SizeLessThanEquals.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.SizeLessThanEquals criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + int size = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); + return Restrictions.sizeLe(propertyName, size); + } + }); + } + + protected void addSimplePropertyCriterionAdapters() { + criterionAdaptors.put(Query.IdEquals.class, new CriterionAdaptor() { + @Override + public org.hibernate.criterion.Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) { + return Restrictions.idEq(((Query.IdEquals) criterion).getValue()); + } + }); + criterionAdaptors.put(Query.Equals.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Equals criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + if (value instanceof DetachedCriteria) { + return Property.forName(propertyName).eq((DetachedCriteria) value); + } + return Restrictions.eq(propertyName, value); + } + }); + criterionAdaptors.put(Query.NotEquals.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.NotEquals criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + if (value instanceof DetachedCriteria) { + return Property.forName(propertyName).ne((DetachedCriteria) value); + } + return Restrictions.ne(propertyName, value); + } + }); + criterionAdaptors.put(Query.GreaterThan.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.GreaterThan criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + if (value instanceof DetachedCriteria) { + return Property.forName(propertyName).gt((DetachedCriteria) value); + } + return Restrictions.gt(propertyName, value); + } + }); + criterionAdaptors.put(Query.GreaterThanEquals.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.GreaterThanEquals criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + if (value instanceof DetachedCriteria) { + return Property.forName(propertyName).ge((DetachedCriteria) value); + } + return Restrictions.ge(propertyName, value); + } + }); + criterionAdaptors.put(Query.LessThan.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.LessThan criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + if (value instanceof DetachedCriteria) { + return Property.forName(propertyName).lt((DetachedCriteria) value); + } + return Restrictions.lt(propertyName, value); + } + }); + criterionAdaptors.put(Query.LessThanEquals.class, new CriterionAdaptor() { + @Override + public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.LessThanEquals criterion, String alias) { + String propertyName = getPropertyName(criterion, alias); + Object value = criterion.getValue(); + if (value instanceof DetachedCriteria) { + return Property.forName(propertyName).le((DetachedCriteria) value); + } + return Restrictions.le(propertyName, value); + } + }); + } + + /** utility methods to group and clean up the initialization of the Criterion Adapters**/ + protected abstract Criterion createRlikeExpression(String propertyName, String pattern); + + protected String getPropertyName(Query.Criterion criterion, String alias) { + return calculatePropertyName(((Query.PropertyNameCriterion) criterion).getProperty(), alias); + } + + protected String calculatePropertyName(String property, String alias) { + if (alias != null) { + return alias + '.' + property; + } + return property; + } + + protected void applySubCriteriaToJunction(PersistentEntity entity, AbstractHibernateQuery hibernateCriteria, List existing, + Junction conjunction, String alias) { + + for (Query.Criterion subCriterion : existing) { + if (subCriterion instanceof Query.PropertyCriterion) { + Query.PropertyCriterion pc = (Query.PropertyCriterion) subCriterion; + if (pc.getValue() instanceof QueryableCriteria) { + pc.setValue(toHibernateDetachedCriteria(hibernateCriteria, (QueryableCriteria) pc.getValue())); + } + else { + AbstractHibernateQuery.doTypeConversionIfNeccessary(entity, pc); + } + } + CriterionAdaptor criterionAdaptor = criterionAdaptors.get(subCriterion.getClass()); + if (criterionAdaptor != null) { + Criterion c = criterionAdaptor.toHibernateCriterion(hibernateCriteria, subCriterion, alias); + if (c != null) + conjunction.add(c); + } + else if (subCriterion instanceof FunctionCallingCriterion) { + Criterion sqlRestriction = hibernateCriteria.getRestrictionForFunctionCall((FunctionCallingCriterion) subCriterion, entity); + if (sqlRestriction != null) { + conjunction.add(sqlRestriction); + } + } + } + } + + public org.hibernate.criterion.Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) { + final CriterionAdaptor criterionAdaptor = criterionAdaptors.get(criterion.getClass()); + if (criterionAdaptor != null) { + return criterionAdaptor.toHibernateCriterion(hibernateQuery, criterion, alias); + } + return null; + } + + protected abstract org.hibernate.criterion.DetachedCriteria toHibernateDetachedCriteria(AbstractHibernateQuery query, QueryableCriteria queryableCriteria); + + protected org.hibernate.criterion.DetachedCriteria toHibernateDetachedCriteria(AbstractHibernateQuery query, QueryableCriteria queryableCriteria, String alias) { + return toHibernateDetachedCriteria(query, queryableCriteria); + } + + public static abstract class CriterionAdaptor { + public abstract org.hibernate.criterion.Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, T criterion, String alias); + + protected Object convertStringValue(Object o) { + if ((!(o instanceof String)) && (o instanceof CharSequence)) { + o = o.toString(); + } + return o; + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java new file mode 100644 index 00000000000..80e69b959f5 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java @@ -0,0 +1,1322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.query; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.persistence.FetchType; +import jakarta.persistence.criteria.JoinType; + +import org.hibernate.Criteria; +import org.hibernate.FetchMode; +import org.hibernate.LockMode; +import org.hibernate.NonUniqueResultException; +import org.hibernate.SessionFactory; +import org.hibernate.criterion.CriteriaSpecification; +import org.hibernate.criterion.DetachedCriteria; +import org.hibernate.criterion.Projections; +import org.hibernate.criterion.Restrictions; +import org.hibernate.criterion.SimpleExpression; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.function.SQLFunction; +import org.hibernate.persister.entity.PropertyMapping; +import org.hibernate.type.BasicType; +import org.hibernate.type.TypeResolver; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.util.ReflectionUtils; + +import org.grails.datastore.gorm.finders.DynamicFinder; +import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria; +import org.grails.datastore.mapping.core.Datastore; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.model.types.Embedded; +import org.grails.datastore.mapping.proxy.ProxyHandler; +import org.grails.datastore.mapping.query.AssociationQuery; +import org.grails.datastore.mapping.query.Query; +import org.grails.datastore.mapping.query.api.QueryableCriteria; +import org.grails.datastore.mapping.query.criteria.FunctionCallingCriterion; +import org.grails.datastore.mapping.query.event.PostQueryEvent; +import org.grails.datastore.mapping.query.event.PreQueryEvent; +import org.grails.orm.hibernate.AbstractHibernateSession; +import org.grails.orm.hibernate.IHibernateTemplate; +import org.grails.orm.hibernate.cfg.AbstractGrailsDomainBinder; +import org.grails.orm.hibernate.cfg.Mapping; +import org.grails.orm.hibernate.proxy.HibernateProxyHandler; + +/** + * Bridges the Query API with the Hibernate Criteria API + * + * @author Graeme Rocher + * @since 1.0 + */ +@SuppressWarnings("rawtypes") +public abstract class AbstractHibernateQuery extends Query { + + public static final String SIZE_CONSTRAINT_PREFIX = "Size"; + + protected static final String ALIAS = "_alias"; + protected static ConversionService conversionService = new DefaultConversionService(); + protected static Field opField = ReflectionUtils.findField(SimpleExpression.class, "op"); + private static final Map JOIN_STATUS_CACHE = new ConcurrentHashMap<>(); + + static { + ReflectionUtils.makeAccessible(opField); + } + + protected Criteria criteria; + protected org.hibernate.criterion.DetachedCriteria detachedCriteria; + protected AbstractHibernateQuery.HibernateProjectionList hibernateProjectionList; + protected String alias; + protected int aliasCount; + protected Map createdAssociationPaths = new HashMap<>(); + protected LinkedList aliasStack = new LinkedList<>(); + protected LinkedList entityStack = new LinkedList<>(); + protected LinkedList associationStack = new LinkedList<>(); + protected LinkedList aliasInstanceStack = new LinkedList(); + private boolean hasJoins = false; + protected ProxyHandler proxyHandler = new HibernateProxyHandler(); + protected final AbstractHibernateCriterionAdapter abstractHibernateCriterionAdapter; + + protected AbstractHibernateQuery(Criteria criteria, AbstractHibernateSession session, PersistentEntity entity) { + super(session, entity); + this.criteria = criteria; + if (entity != null) { + initializeJoinStatus(); + } + this.abstractHibernateCriterionAdapter = createHibernateCriterionAdapter(); + } + + protected AbstractHibernateQuery(DetachedCriteria criteria, PersistentEntity entity) { + super(null, entity); + this.detachedCriteria = criteria; + this.abstractHibernateCriterionAdapter = createHibernateCriterionAdapter(); + if (entity != null) { + initializeJoinStatus(); + } + } + + @Override + protected Object resolveIdIfEntity(Object value) { + // for Hibernate queries, the object itself is used in queries, not the id + return value; + } + + protected void initializeJoinStatus() { + Boolean cachedStatus = JOIN_STATUS_CACHE.get(entity.getName()); + if (cachedStatus != null) hasJoins = cachedStatus; + else { + for (Association a : entity.getAssociations()) { + if (a.getFetchStrategy() == FetchType.EAGER) hasJoins = true; + } + } + } + + protected AbstractHibernateQuery(Criteria subCriteria, AbstractHibernateSession session, PersistentEntity associatedEntity, String newAlias) { + this(subCriteria, session, associatedEntity); + alias = newAlias; + } + + @Override + public Query isEmpty(String property) { + org.hibernate.criterion.Criterion criterion = Restrictions.isEmpty(calculatePropertyName(property)); + addToCriteria(criterion); + return this; + } + + @Override + public Query isNotEmpty(String property) { + addToCriteria(Restrictions.isNotEmpty(calculatePropertyName(property))); + return this; + } + + @Override + public Query isNull(String property) { + addToCriteria(Restrictions.isNull(calculatePropertyName(property))); + return this; + } + + @Override + public Query isNotNull(String property) { + addToCriteria(Restrictions.isNotNull(calculatePropertyName(property))); + return this; + } + + @Override + public void add(Criterion criterion) { + if (criterion instanceof FunctionCallingCriterion) { + org.hibernate.criterion.Criterion sqlRestriction = getRestrictionForFunctionCall((FunctionCallingCriterion) criterion, getEntity()); + if (sqlRestriction != null) { + addToCriteria(sqlRestriction); + } + } + else if (criterion instanceof PropertyCriterion) { + PropertyCriterion pc = (PropertyCriterion) criterion; + Object value = pc.getValue(); + if (value instanceof QueryableCriteria) { + setDetachedCriteriaValue((QueryableCriteria) value, pc); + } else { + if (!(value instanceof DetachedCriteria)) { + doTypeConversionIfNeccessary(getEntity(), pc); + } + } + } + if (criterion instanceof DetachedAssociationCriteria) { + DetachedAssociationCriteria associationCriteria = (DetachedAssociationCriteria) criterion; + + Association association = associationCriteria.getAssociation(); + List criteria = associationCriteria.getCriteria(); + + if (association instanceof Embedded) { + String associationName = association.getName(); + if (getCurrentAlias() != null) { + associationName = getCurrentAlias() + '.' + associationName; + } + for (Criterion c : criteria) { + final org.hibernate.criterion.Criterion hibernateCriterion = getHibernateCriterionAdapter().toHibernateCriterion(this, c, associationName); + if (hibernateCriterion != null) { + addToCriteria(hibernateCriterion); + } + } + } + else { + + CriteriaAndAlias criteriaAndAlias = getCriteriaAndAlias(associationCriteria); + + if (criteriaAndAlias.criteria != null) { + aliasInstanceStack.add(criteriaAndAlias.criteria); + } + else if (criteriaAndAlias.detachedCriteria != null) { + aliasInstanceStack.add(criteriaAndAlias.detachedCriteria); + } + aliasStack.add(criteriaAndAlias.alias); + associationStack.add(association); + entityStack.add(association.getAssociatedEntity()); + + try { + @SuppressWarnings("unchecked") + List associationCriteriaList = criteria; + for (Criterion c : associationCriteriaList) { + add(c); + } + } + finally { + aliasInstanceStack.removeLast(); + aliasStack.removeLast(); + entityStack.removeLast(); + associationStack.removeLast(); + } + } + + } + else { + + final org.hibernate.criterion.Criterion hibernateCriterion = getHibernateCriterionAdapter().toHibernateCriterion(this, criterion, getCurrentAlias()); + if (hibernateCriterion != null) { + addToCriteria(hibernateCriterion); + } + } + } + + @Override + public PersistentEntity getEntity() { + if (!entityStack.isEmpty()) { + return entityStack.getLast(); + } + return super.getEntity(); + } + + protected String getAssociationPath(String propertyName) { + if (propertyName.indexOf('.') > -1) { + return propertyName; + } + else { + + StringBuilder fullPath = new StringBuilder(); + for (Association association : associationStack) { + fullPath.append(association.getName()); + fullPath.append('.'); + } + fullPath.append(propertyName); + return fullPath.toString(); + } + } + + protected String getCurrentAlias() { + if (alias != null) { + return alias; + } + + if (aliasStack.isEmpty()) { + return null; + } + + return aliasStack.getLast(); + } + + @SuppressWarnings("unchecked") + static void doTypeConversionIfNeccessary(PersistentEntity entity, PropertyCriterion pc) { + // ignore Size related constraints + if (pc.getClass().getSimpleName().startsWith(SIZE_CONSTRAINT_PREFIX)) { + return; + } + + String property = pc.getProperty(); + Object value = pc.getValue(); + PersistentProperty p = entity.getPropertyByName(property); + if (p != null && !p.getType().isInstance(value)) { + pc.setValue(conversionService.convert(value, p.getType())); + } + } + + org.hibernate.criterion.Criterion getRestrictionForFunctionCall(FunctionCallingCriterion criterion, PersistentEntity entity) { + org.hibernate.criterion.Criterion sqlRestriction; + + SessionFactory sessionFactory = ((IHibernateTemplate) session.getNativeInterface()).getSessionFactory(); + String property = criterion.getProperty(); + Criterion datastoreCriterion = criterion.getPropertyCriterion(); + PersistentProperty pp = entity.getPropertyByName(property); + + if (pp == null) throw new InvalidDataAccessResourceUsageException( + "Cannot execute function defined in query [" + criterion.getFunctionName() + + "] on non-existent property [" + property + "] of [" + entity.getJavaClass() + "]"); + + String functionName = criterion.getFunctionName(); + + Dialect dialect = getDialect(sessionFactory); + SQLFunction sqlFunction = dialect.getFunctions().get(functionName); + if (sqlFunction != null) { + TypeResolver typeResolver = getTypeResolver(sessionFactory); + BasicType basic = typeResolver.basic(pp.getType().getName()); + if (basic != null && datastoreCriterion instanceof PropertyCriterion) { + + PropertyCriterion pc = (PropertyCriterion) datastoreCriterion; + final org.hibernate.criterion.Criterion hibernateCriterion = getHibernateCriterionAdapter().toHibernateCriterion(this, datastoreCriterion, alias); + if (hibernateCriterion instanceof SimpleExpression) { + SimpleExpression expr = (SimpleExpression) hibernateCriterion; + Object op = ReflectionUtils.getField(opField, expr); + PropertyMapping mapping = getEntityPersister(entity.getJavaClass().getName(), sessionFactory); + String[] columns; + if (alias != null) { + columns = mapping.toColumns(alias, property); + } + else { + columns = mapping.toColumns(property); + } + String root = render(basic, Arrays.asList(columns), sessionFactory, sqlFunction); + Object value = pc.getValue(); + if (value != null) { + sqlRestriction = Restrictions.sqlRestriction(root + op + "?", value, typeResolver.basic(value.getClass().getName())); + } + else { + sqlRestriction = Restrictions.sqlRestriction(root + op + "?", value, basic); + } + } + else { + throw new InvalidDataAccessResourceUsageException("Unsupported function [" + functionName + "] defined in query for property [" + property + "] with type [" + pp.getType() + "]"); + } + } + else { + throw new InvalidDataAccessResourceUsageException("Unsupported function [" + functionName + "] defined in query for property [" + property + "] with type [" + pp.getType() + "]"); + } + } + else { + throw new InvalidDataAccessResourceUsageException("Unsupported function defined in query [" + functionName + "]"); + } + return sqlRestriction; + } + + protected abstract String render(BasicType basic, List asList, SessionFactory sessionFactory, SQLFunction sqlFunction); + + protected abstract PropertyMapping getEntityPersister(String name, SessionFactory sessionFactory); + + protected abstract TypeResolver getTypeResolver(SessionFactory sessionFactory); + + protected abstract Dialect getDialect(SessionFactory sessionFactory); + + @Override + public Junction disjunction() { + final org.hibernate.criterion.Disjunction disjunction = Restrictions.disjunction(); + addToCriteria(disjunction); + return new HibernateJunction(disjunction, alias); + } + + @Override + public Junction negation() { + final org.hibernate.criterion.Disjunction disjunction = Restrictions.disjunction(); + addToCriteria(Restrictions.not(disjunction)); + return new HibernateJunction(disjunction, alias); + } + + @Override + public Query eq(String property, Object value) { + addToCriteria(Restrictions.eq(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query idEq(Object value) { + addToCriteria(Restrictions.idEq(value)); + return this; + } + + @Override + public Query gt(String property, Object value) { + addToCriteria(Restrictions.gt(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query and(Criterion a, Criterion b) { + AbstractHibernateCriterionAdapter adapter = getHibernateCriterionAdapter(); + addToCriteria(Restrictions.and(adapter.toHibernateCriterion(this, a, alias), adapter.toHibernateCriterion(this, a, alias))); + return this; + } + + @Override + public Query or(Criterion a, Criterion b) { + AbstractHibernateCriterionAdapter adapter = getHibernateCriterionAdapter(); + addToCriteria(Restrictions.or(adapter.toHibernateCriterion(this, a, alias), adapter.toHibernateCriterion(this, b, alias))); + return this; + } + + @Override + public Query allEq(Map values) { + addToCriteria(Restrictions.allEq(values)); + return this; + } + + @Override + public Query ge(String property, Object value) { + addToCriteria(Restrictions.ge(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query le(String property, Object value) { + addToCriteria(Restrictions.le(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query gte(String property, Object value) { + addToCriteria(Restrictions.ge(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query lte(String property, Object value) { + addToCriteria(Restrictions.le(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query lt(String property, Object value) { + addToCriteria(Restrictions.lt(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query in(String property, List values) { + addToCriteria(Restrictions.in(calculatePropertyName(property), values)); + return this; + } + + @Override + public Query between(String property, Object start, Object end) { + addToCriteria(Restrictions.between(calculatePropertyName(property), start, end)); + return this; + } + + @Override + public Query like(String property, String expr) { + addToCriteria(Restrictions.like(calculatePropertyName(property), calculatePropertyName(expr))); + return this; + } + + @Override + public Query ilike(String property, String expr) { + addToCriteria(Restrictions.ilike(calculatePropertyName(property), calculatePropertyName(expr))); + return this; + } + + @Override + public Query rlike(String property, String expr) { + addToCriteria(createRlikeExpression(calculatePropertyName(property), calculatePropertyName(expr))); + return this; + } + + @Override + public AssociationQuery createQuery(String associationName) { + final PersistentProperty property = entity.getPropertyByName(calculatePropertyName(associationName)); + if (property != null && (property instanceof Association)) { + String alias = generateAlias(associationName); + CriteriaAndAlias subCriteria = getOrCreateAlias(associationName, alias); + + Association association = (Association) property; + if (subCriteria.criteria != null) { + return new HibernateAssociationQuery(subCriteria.criteria, (AbstractHibernateSession) getSession(), association.getAssociatedEntity(), association, alias); + } + else if (subCriteria.detachedCriteria != null) { + return new HibernateAssociationQuery(subCriteria.detachedCriteria, (AbstractHibernateSession) getSession(), association.getAssociatedEntity(), association, alias); + } + } + throw new InvalidDataAccessApiUsageException("Cannot query association [" + calculatePropertyName(associationName) + "] of entity [" + entity + "]. Property is not an association!"); + } + + protected CriteriaAndAlias getCriteriaAndAlias(DetachedAssociationCriteria associationCriteria) { + String associationPath = associationCriteria.getAssociationPath(); + String alias = associationCriteria.getAlias(); + + if (associationPath == null) { + associationPath = associationCriteria.getAssociation().getName(); + } + return getOrCreateAlias(associationPath, alias); + } + + protected CriteriaAndAlias getOrCreateAlias(String associationName, String alias) { + CriteriaAndAlias subCriteria = null; + String associationPath = getAssociationPath(associationName); + Criteria parentCriteria = criteria; + if (alias == null) { + alias = generateAlias(associationName); + } + else { + CriteriaAndAlias criteriaAndAlias = createdAssociationPaths.get(alias); + if (criteriaAndAlias != null) { + parentCriteria = criteriaAndAlias.criteria; + if (parentCriteria != null) { + + alias = associationName + '_' + alias; + associationPath = criteriaAndAlias.associationPath + '.' + associationPath; + } + } + } + if (createdAssociationPaths.containsKey(associationName)) { + subCriteria = createdAssociationPaths.get(associationName); + } + else { + JoinType joinType = joinTypes.get(associationName); + if (parentCriteria != null) { + Criteria sc = parentCriteria.createAlias(associationPath, alias, resolveJoinType(joinType)); + subCriteria = new CriteriaAndAlias(sc, alias, associationPath); + } + else if (detachedCriteria != null) { + DetachedCriteria sc = detachedCriteria.createAlias(associationPath, alias, resolveJoinType(joinType)); + subCriteria = new CriteriaAndAlias(sc, alias, associationPath); + } + if (subCriteria != null) { + + createdAssociationPaths.put(associationPath, subCriteria); + createdAssociationPaths.put(alias, subCriteria); + } + } + return subCriteria; + } + + private org.hibernate.sql.JoinType resolveJoinType(JoinType joinType) { + if (joinType == null) { + return org.hibernate.sql.JoinType.INNER_JOIN; + } + switch (joinType) { + case LEFT: + return org.hibernate.sql.JoinType.LEFT_OUTER_JOIN; + case RIGHT: + return org.hibernate.sql.JoinType.RIGHT_OUTER_JOIN; + default: + return org.hibernate.sql.JoinType.INNER_JOIN; + } + } + + @Override + public ProjectionList projections() { + if (hibernateProjectionList == null) { + hibernateProjectionList = new HibernateProjectionList(); + } + return hibernateProjectionList; + } + + @Override + public Query max(int max) { + if (criteria != null) + criteria.setMaxResults(max); + return this; + } + + @Override + public Query maxResults(int max) { + if (criteria != null) + criteria.setMaxResults(max); + return this; + } + + @Override + public Query offset(int offset) { + if (criteria != null) + criteria.setFirstResult(offset); + return this; + } + + @Override + public Query firstResult(int offset) { + offset(offset); + return this; + } + + @Override + public Query cache(boolean cache) { + criteria.setCacheable(cache); + + return super.cache(cache); + } + + @Override + public Query lock(boolean lock) { + criteria.setCacheable(false); + criteria.setLockMode(LockMode.PESSIMISTIC_WRITE); + return super.lock(lock); + } + + @Override + public Query order(Order order) { + super.order(order); + + String property = order.getProperty(); + + int i = property.indexOf('.'); + if (i > -1) { + + String sortHead = property.substring(0, i); + String sortTail = property.substring(i + 1); + + if (createdAssociationPaths.containsKey(sortHead)) { + CriteriaAndAlias criteriaAndAlias = createdAssociationPaths.get(sortHead); + Criteria criteria = criteriaAndAlias.criteria; + org.hibernate.criterion.Order hibernateOrder = order.getDirection() == Order.Direction.ASC ? + org.hibernate.criterion.Order.asc(property) : + org.hibernate.criterion.Order.desc(property); + + criteria.addOrder(order.isIgnoreCase() ? hibernateOrder.ignoreCase() : hibernateOrder); + } + else { + + PersistentProperty persistentProperty = entity.getPropertyByName(sortHead); + + if (persistentProperty instanceof Association) { + Association a = (Association) persistentProperty; + if (persistentProperty instanceof Embedded) { + addSimpleOrder(order, property); + } + else { + if (criteria != null) { + Criteria subCriteria = criteria.createCriteria(sortHead); + addOrderToCriteria(subCriteria, sortTail, order); + } + else if (detachedCriteria != null) { + DetachedCriteria subDetachedCriteria = detachedCriteria.createCriteria(sortHead); + addOrderToDetachedCriteria(subDetachedCriteria, sortTail, order); + } + } + } + } + + } + else { + addSimpleOrder(order, property); + } + + return this; + } + + private void addSimpleOrder(Order order, String property) { + Criteria c = criteria; + if (c != null) { + addOrderToCriteria(c, property, order); + } else { + DetachedCriteria dc = detachedCriteria; + addOrderToDetachedCriteria(dc, property, order); + } + } + + private void addOrderToDetachedCriteria(DetachedCriteria dc, String property, Order order) { + if (dc != null) { + org.hibernate.criterion.Order hibernateOrder = order.getDirection() == Order.Direction.ASC ? + org.hibernate.criterion.Order.asc(calculatePropertyName(property)) : + org.hibernate.criterion.Order.desc(calculatePropertyName(property)); + dc.addOrder(order.isIgnoreCase() ? hibernateOrder.ignoreCase() : hibernateOrder); + + } + } + + private void addOrderToCriteria(Criteria c, String property, Order order) { + org.hibernate.criterion.Order hibernateOrder = order.getDirection() == Order.Direction.ASC ? + org.hibernate.criterion.Order.asc(calculatePropertyName(property)) : + org.hibernate.criterion.Order.desc(calculatePropertyName(property)); + + c.addOrder(order.isIgnoreCase() ? hibernateOrder.ignoreCase() : hibernateOrder); + } + + private String calculateProjectionPropertyName(String propertyName) { + int firstDot = propertyName.indexOf('.'); + if (firstDot < 0) { + return calculatePropertyName(propertyName); + } + + PersistentEntity currentEntity = getEntity(); + String currentAlias = null; + StringBuilder associationPath = new StringBuilder(); + String[] tokens = propertyName.split("\\."); + + for (int i = 0; i < tokens.length - 1; i++) { + String token = tokens[i]; + PersistentProperty persistentProperty = currentEntity != null ? currentEntity.getPropertyByName(token) : null; + if (!(persistentProperty instanceof Association) || persistentProperty instanceof Embedded) { + return calculatePropertyName(propertyName); + } + + if (associationPath.length() > 0) { + associationPath.append('.'); + } + associationPath.append(token); + + // Use LEFT JOIN for auto-created projection aliases so that rows + // with null associations are preserved in the result set. + String path = associationPath.toString(); + if (!joinTypes.containsKey(path)) { + joinTypes.put(path, JoinType.LEFT); + } + CriteriaAndAlias criteriaAndAlias = getOrCreateAlias(path, generateAlias(token)); + if (criteriaAndAlias == null) { + return calculatePropertyName(propertyName); + } + currentAlias = criteriaAndAlias.alias; + currentEntity = ((Association) persistentProperty).getAssociatedEntity(); + } + + if (currentAlias == null) { + return calculatePropertyName(propertyName); + } + return currentAlias + '.' + tokens[tokens.length - 1]; + } + + private Query.Projection normalizeProjectionPropertyPath(Query.Projection projection) { + if (!(projection instanceof Query.PropertyProjection)) { + return projection; + } + + String propertyName = ((Query.PropertyProjection) projection).getPropertyName(); + String normalizedPropertyName = calculateProjectionPropertyName(propertyName); + if (propertyName.equals(normalizedPropertyName)) { + return projection; + } + + if (projection instanceof Query.DistinctPropertyProjection) { + return org.grails.datastore.mapping.query.Projections.distinct(normalizedPropertyName); + } + if (projection instanceof Query.CountDistinctProjection) { + return org.grails.datastore.mapping.query.Projections.countDistinct(normalizedPropertyName); + } + if (projection instanceof Query.GroupPropertyProjection) { + return org.grails.datastore.mapping.query.Projections.groupProperty(normalizedPropertyName); + } + if (projection instanceof Query.SumProjection) { + return org.grails.datastore.mapping.query.Projections.sum(normalizedPropertyName); + } + if (projection instanceof Query.MinProjection) { + return org.grails.datastore.mapping.query.Projections.min(normalizedPropertyName); + } + if (projection instanceof Query.MaxProjection) { + return org.grails.datastore.mapping.query.Projections.max(normalizedPropertyName); + } + if (projection instanceof Query.AvgProjection) { + return org.grails.datastore.mapping.query.Projections.avg(normalizedPropertyName); + } + return org.grails.datastore.mapping.query.Projections.property(normalizedPropertyName); + } + + @Override + public Query join(String property) { + this.hasJoins = true; + if (criteria != null) + criteria.setFetchMode(property, FetchMode.JOIN); + else if (detachedCriteria != null) + detachedCriteria.setFetchMode(property, FetchMode.JOIN); + return this; + } + + @Override + public Query join(String property, JoinType joinType) { + this.hasJoins = true; + this.joinTypes.put(property, joinType); + if (criteria != null) + criteria.setFetchMode(property, FetchMode.JOIN); + else if (detachedCriteria != null) + detachedCriteria.setFetchMode(property, FetchMode.JOIN); + return this; + } + + @Override + public Query select(String property) { + this.hasJoins = true; + if (criteria != null) + criteria.setFetchMode(property, FetchMode.SELECT); + else if (detachedCriteria != null) + detachedCriteria.setFetchMode(property, FetchMode.SELECT); + return this; + } + + @Override + public List list() { + if (criteria == null) throw new IllegalStateException("Cannot execute query using a detached criteria instance"); + + int projectionLength = 0; + if (hibernateProjectionList != null) { + org.hibernate.criterion.ProjectionList projectionList = hibernateProjectionList.getHibernateProjectionList(); + projectionLength = projectionList.getLength(); + if (projectionLength > 0) { + criteria.setProjection(projectionList); + } + } + + if (projectionLength < 2) { + criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY); + } + + applyDefaultSortOrderAndCaching(); + applyFetchStrategies(); + + return listForCriteria(); + } + + public List listForCriteria() { + Datastore datastore = session.getDatastore(); + ApplicationEventPublisher publisher = datastore.getApplicationEventPublisher(); + if (publisher != null) { + publisher.publishEvent(new PreQueryEvent(datastore, this)); + } + + List results = criteria.list(); + if (publisher != null) { + publisher.publishEvent(new PostQueryEvent(datastore, this, results)); + } + return results; + } + + protected void applyDefaultSortOrderAndCaching() { + if (this.orderBy.isEmpty() && entity != null) { + // don't apply default sorting, if projections present + if (hibernateProjectionList != null && !hibernateProjectionList.isEmpty()) return; + + Mapping mapping = AbstractGrailsDomainBinder.getMapping(entity.getJavaClass()); + if (mapping != null) { + if (queryCache == null && mapping.getCache() != null && mapping.getCache().isEnabled()) { + criteria.setCacheable(true); + } + + Map sortMap = mapping.getSort().getNamesAndDirections(); + DynamicFinder.applySortForMap(this, sortMap, true); + + } + } + } + + protected void applyFetchStrategies() { + for (Map.Entry entry : fetchStrategies.entrySet()) { + switch (entry.getValue()) { + case EAGER: + if (criteria != null) + criteria.setFetchMode(entry.getKey(), FetchMode.JOIN); + else if (detachedCriteria != null) + detachedCriteria.setFetchMode(entry.getKey(), FetchMode.JOIN); + break; + case LAZY: + if (criteria != null) + criteria.setFetchMode(entry.getKey(), FetchMode.SELECT); + else if (detachedCriteria != null) + detachedCriteria.setFetchMode(entry.getKey(), FetchMode.SELECT); + break; + } + } + } + + @Override + protected void flushBeforeQuery() { + // do nothing + } + + @Override + public Object singleResult() { + if (criteria == null) throw new IllegalStateException("Cannot execute query using a detached criteria instance"); + + if (hibernateProjectionList != null) { + criteria.setProjection(hibernateProjectionList.getHibernateProjectionList()); + } + criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY); + applyDefaultSortOrderAndCaching(); + applyFetchStrategies(); + + Datastore datastore = session.getDatastore(); + ApplicationEventPublisher publisher = datastore.getApplicationEventPublisher(); + if (publisher != null) { + publisher.publishEvent(new PreQueryEvent(datastore, this)); + } + + Object result; + if (hasJoins) { + try { + result = proxyHandler.unwrap(criteria.uniqueResult());; + } catch (NonUniqueResultException e) { + result = singleResultViaListCall(); + } + } + else { + result = singleResultViaListCall(); + } + if (publisher != null) { + publisher.publishEvent(new PostQueryEvent(datastore, this, Collections.singletonList(result))); + } + return result; + } + + private Object singleResultViaListCall() { + criteria.setMaxResults(1); + if (hibernateProjectionList != null && hibernateProjectionList.isRowCount()) { + criteria.setFirstResult(0); + } + List results = criteria.list(); + if (results.size() > 0) { + return proxyHandler.unwrap(results.get(0)); + } + return null; + } + + @Override + protected List executeQuery(PersistentEntity entity, Junction criteria) { + return list(); + } + + String handleAssociationQuery(Association association, List criteriaList) { + return getCriteriaAndAlias(association).alias; + } + + String handleAssociationQuery(Association association, List criteriaList, String alias) { + String associationName = calculatePropertyName(association.getName()); + return getOrCreateAlias(associationName, alias).alias; + } + + protected CriteriaAndAlias getCriteriaAndAlias(Association association) { + String associationName = calculatePropertyName(association.getName()); + String newAlias = generateAlias(associationName); + return getOrCreateAlias(associationName, newAlias); + } + + protected void addToCriteria(org.hibernate.criterion.Criterion criterion) { + if (criterion == null) { + return; + } + + if (aliasInstanceStack.isEmpty()) { + if (criteria != null) { + criteria.add(criterion); + + } + else if (detachedCriteria != null) { + detachedCriteria.add(criterion); + } + } + else { + Object criteriaObject = aliasInstanceStack.getLast(); + if (criteriaObject instanceof Criteria) + ((Criteria) criteriaObject).add(criterion); + else if (criteriaObject instanceof DetachedCriteria) { + ((DetachedCriteria) criteriaObject).add(criterion); + } + } + } + + protected String calculatePropertyName(String property) { + if (alias == null) { + return property; + } + return alias + '.' + property; + } + + protected String generateAlias(String associationName) { + return calculatePropertyName(associationName) + calculatePropertyName(ALIAS) + aliasCount++; + } + + protected abstract void setDetachedCriteriaValue(QueryableCriteria value, PropertyCriterion pc); + + protected AbstractHibernateCriterionAdapter getHibernateCriterionAdapter() { + return this.abstractHibernateCriterionAdapter; + } + + protected abstract AbstractHibernateCriterionAdapter createHibernateCriterionAdapter(); + + protected abstract org.hibernate.criterion.Criterion createRlikeExpression(String propertyName, String value); + + protected class HibernateJunction extends Junction { + + protected org.hibernate.criterion.Junction hibernateJunction; + protected String alias; + + public HibernateJunction(org.hibernate.criterion.Junction junction, String alias) { + hibernateJunction = junction; + this.alias = alias; + } + + @Override + public Junction add(Criterion c) { + if (c != null) { + if (c instanceof FunctionCallingCriterion) { + org.hibernate.criterion.Criterion sqlRestriction = getRestrictionForFunctionCall((FunctionCallingCriterion) c, entity); + if (sqlRestriction != null) { + hibernateJunction.add(sqlRestriction); + } + } + else { + AbstractHibernateCriterionAdapter adapter = getHibernateCriterionAdapter(); + org.hibernate.criterion.Criterion criterion = adapter.toHibernateCriterion(AbstractHibernateQuery.this, c, alias); + if (criterion != null) { + hibernateJunction.add(criterion); + } + } + } + return this; + } + } + + protected class HibernateProjectionList extends ProjectionList { + + org.hibernate.criterion.ProjectionList projectionList = Projections.projectionList(); + private boolean rowCount = false; + + public boolean isRowCount() { + return rowCount; + } + + public org.hibernate.criterion.ProjectionList getHibernateProjectionList() { + return projectionList; + } + + @Override + public boolean isEmpty() { + return projectionList.getLength() == 0; + } + + @Override + public ProjectionList add(Projection p) { + projectionList.add(new HibernateProjectionAdapter(normalizeProjectionPropertyPath(p)).toHibernateProjection()); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.ProjectionList countDistinct(String property) { + projectionList.add(Projections.countDistinct(calculateProjectionPropertyName(property))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.ProjectionList distinct(String property) { + projectionList.add(Projections.distinct(Projections.property(calculateProjectionPropertyName(property)))); + return this; + } + + @Override + public org.grails.datastore.mapping.query.api.ProjectionList rowCount() { + projectionList.add(Projections.rowCount()); + this.rowCount = true; + return this; + } + + @Override + public ProjectionList id() { + projectionList.add(Projections.id()); + return this; + } + + @Override + public ProjectionList count() { + projectionList.add(Projections.rowCount()); + this.rowCount = true; + return this; + } + + @Override + public ProjectionList property(String name) { + projectionList.add(Projections.property(calculateProjectionPropertyName(name))); + return this; + } + + @Override + public ProjectionList sum(String name) { + projectionList.add(Projections.sum(calculateProjectionPropertyName(name))); + return this; + } + + @Override + public ProjectionList min(String name) { + projectionList.add(Projections.min(calculateProjectionPropertyName(name))); + return this; + } + + @Override + public ProjectionList max(String name) { + projectionList.add(Projections.max(calculateProjectionPropertyName(name))); + return this; + } + + @Override + public ProjectionList avg(String name) { + projectionList.add(Projections.avg(calculateProjectionPropertyName(name))); + return this; + } + + @Override + public ProjectionList distinct() { + if (criteria != null) + criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); + else if (detachedCriteria != null) + detachedCriteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); + return this; + } + } + + protected class HibernateAssociationQuery extends AssociationQuery { + + protected String alias; + protected org.hibernate.criterion.Junction hibernateJunction; + protected Criteria assocationCriteria; + protected DetachedCriteria detachedAssocationCriteria; + + public HibernateAssociationQuery(Criteria criteria, AbstractHibernateSession session, PersistentEntity associatedEntity, Association association, String alias) { + super(session, associatedEntity, association); + this.alias = alias; + assocationCriteria = criteria; + } + + public HibernateAssociationQuery(DetachedCriteria criteria, AbstractHibernateSession session, PersistentEntity associatedEntity, Association association, String alias) { + super(session, associatedEntity, association); + this.alias = alias; + detachedAssocationCriteria = criteria; + } + + @Override + public Query order(Order order) { + + Order.Direction direction = order.getDirection(); + switch (direction) { + case ASC: + assocationCriteria.addOrder(org.hibernate.criterion.Order.asc(order.getProperty())); + case DESC: + assocationCriteria.addOrder(org.hibernate.criterion.Order.desc(order.getProperty())); + } + return super.order(order); + } + + @Override + public Query isEmpty(String property) { + org.hibernate.criterion.Criterion criterion = Restrictions.isEmpty(calculatePropertyName(property)); + addToCriteria(criterion); + return this; + } + + protected void addToCriteria(org.hibernate.criterion.Criterion criterion) { + if (hibernateJunction != null) { + hibernateJunction.add(criterion); + } + else if (assocationCriteria != null) { + assocationCriteria.add(criterion); + } + else if (detachedAssocationCriteria != null) { + detachedAssocationCriteria.add(criterion); + } + } + + @Override + public Query isNotEmpty(String property) { + addToCriteria(Restrictions.isNotEmpty(calculatePropertyName(property))); + return this; + } + + @Override + public Query isNull(String property) { + addToCriteria(Restrictions.isNull(calculatePropertyName(property))); + return this; + } + + @Override + public Query isNotNull(String property) { + addToCriteria(Restrictions.isNotNull(calculatePropertyName(property))); + return this; + } + + @Override + public void add(Criterion criterion) { + final org.hibernate.criterion.Criterion hibernateCriterion = getHibernateCriterionAdapter().toHibernateCriterion(AbstractHibernateQuery.this, criterion, alias); + if (hibernateCriterion != null) { + addToCriteria(hibernateCriterion); + } + } + + @Override + public Junction disjunction() { + final org.hibernate.criterion.Disjunction disjunction = Restrictions.disjunction(); + addToCriteria(disjunction); + return new HibernateJunction(disjunction, alias); + } + + @Override + public Junction negation() { + final org.hibernate.criterion.Disjunction disjunction = Restrictions.disjunction(); + addToCriteria(Restrictions.not(disjunction)); + return new HibernateJunction(disjunction, alias); + } + + @Override + public Query eq(String property, Object value) { + addToCriteria(Restrictions.eq(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query idEq(Object value) { + addToCriteria(Restrictions.idEq(value)); + return this; + } + + @Override + public Query gt(String property, Object value) { + addToCriteria(Restrictions.gt(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query and(Criterion a, Criterion b) { + AbstractHibernateCriterionAdapter adapter = getHibernateCriterionAdapter(); + addToCriteria(Restrictions.and(adapter.toHibernateCriterion(AbstractHibernateQuery.this, a, alias), adapter.toHibernateCriterion(AbstractHibernateQuery.this, b, alias))); + return this; + } + + @Override + public Query or(Criterion a, Criterion b) { + AbstractHibernateCriterionAdapter adapter = getHibernateCriterionAdapter(); + addToCriteria(Restrictions.or(adapter.toHibernateCriterion(AbstractHibernateQuery.this, a, alias), adapter.toHibernateCriterion(AbstractHibernateQuery.this, b, alias))); + return this; + } + + @Override + public Query allEq(Map values) { + addToCriteria(Restrictions.allEq(values)); + return this; + } + + @Override + public Query ge(String property, Object value) { + addToCriteria(Restrictions.ge(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query le(String property, Object value) { + addToCriteria(Restrictions.le(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query gte(String property, Object value) { + addToCriteria(Restrictions.ge(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query lte(String property, Object value) { + addToCriteria(Restrictions.le(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query lt(String property, Object value) { + addToCriteria(Restrictions.lt(calculatePropertyName(property), value)); + return this; + } + + @Override + public Query in(String property, List values) { + addToCriteria(Restrictions.in(calculatePropertyName(property), values)); + return this; + } + + @Override + public Query between(String property, Object start, Object end) { + addToCriteria(Restrictions.between(calculatePropertyName(property), start, end)); + return this; + } + + @Override + public Query like(String property, String expr) { + addToCriteria(Restrictions.like(calculatePropertyName(property), calculatePropertyName(expr))); + return this; + } + + @Override + public Query ilike(String property, String expr) { + addToCriteria(Restrictions.ilike(calculatePropertyName(property), calculatePropertyName(expr))); + return this; + } + + @Override + public Query rlike(String property, String expr) { + addToCriteria(createRlikeExpression(calculatePropertyName(property), calculatePropertyName(expr))); + return this; + } + } + + protected class CriteriaAndAlias { + protected DetachedCriteria detachedCriteria; + protected Criteria criteria; + protected String alias; + protected String associationPath; + + public CriteriaAndAlias(DetachedCriteria detachedCriteria, String alias, String associationPath) { + this.detachedCriteria = detachedCriteria; + this.alias = alias; + this.associationPath = associationPath; + } + + public CriteriaAndAlias(Criteria criteria, String alias, String associationPath) { + this.criteria = criteria; + this.alias = alias; + this.associationPath = associationPath; + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java new file mode 100644 index 00000000000..2a6e6406a87 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsHibernateQueryUtils.java @@ -0,0 +1,431 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.query; + +import java.util.Map; + +import jakarta.persistence.LockModeType; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; + +import org.hibernate.Criteria; +import org.hibernate.FetchMode; +import org.hibernate.FlushMode; +import org.hibernate.LockMode; +import org.hibernate.criterion.Order; +import org.hibernate.query.Query; + +import org.springframework.core.convert.ConversionService; + +import org.grails.datastore.gorm.finders.DynamicFinder; +import org.grails.datastore.mapping.config.Property; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.types.Association; +import org.grails.datastore.mapping.model.types.Embedded; +import org.grails.datastore.mapping.reflect.ClassUtils; +import org.grails.orm.hibernate.cfg.AbstractGrailsDomainBinder; +import org.grails.orm.hibernate.cfg.Mapping; + +/** + * Utility methods for configuring Hibernate queries + * + * @author Graeme Rocher + * @since 4.0 + */ +public class GrailsHibernateQueryUtils { + + /** + * Populates criteria arguments for the given target class and arguments map + * + * @param entity The {@link org.grails.datastore.mapping.model.PersistentEntity} instance + * @param c The criteria instance + * @param argMap The arguments map + */ + @SuppressWarnings("rawtypes") + @Deprecated + public static void populateArgumentsForCriteria(PersistentEntity entity, Criteria c, Map argMap, ConversionService conversionService, boolean useDefaultMapping) { + Integer maxParam = null; + Integer offsetParam = null; + if (argMap.containsKey(DynamicFinder.ARGUMENT_MAX)) { + maxParam = conversionService.convert(argMap.get(DynamicFinder.ARGUMENT_MAX), Integer.class); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_OFFSET)) { + offsetParam = conversionService.convert(argMap.get(DynamicFinder.ARGUMENT_OFFSET), Integer.class); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_FETCH_SIZE)) { + c.setFetchSize(conversionService.convert(argMap.get(DynamicFinder.ARGUMENT_FETCH_SIZE), Integer.class)); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_TIMEOUT)) { + c.setTimeout(conversionService.convert(argMap.get(DynamicFinder.ARGUMENT_TIMEOUT), Integer.class)); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_FLUSH_MODE)) { + c.setFlushMode(convertFlushMode(argMap.get(DynamicFinder.ARGUMENT_FLUSH_MODE))); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_READ_ONLY)) { + c.setReadOnly(ClassUtils.getBooleanFromMap(DynamicFinder.ARGUMENT_READ_ONLY, argMap)); + } + String orderParam = (String) argMap.get(DynamicFinder.ARGUMENT_ORDER); + Object fetchObj = argMap.get(DynamicFinder.ARGUMENT_FETCH); + if (fetchObj instanceof Map) { + Map fetch = (Map) fetchObj; + for (Object o : fetch.keySet()) { + String associationName = (String) o; + c.setFetchMode(associationName, getFetchMode(fetch.get(associationName))); + } + } + + final int max = maxParam == null ? -1 : maxParam; + final int offset = offsetParam == null ? -1 : offsetParam; + if (max > -1) { + c.setMaxResults(max); + } + if (offset > -1) { + c.setFirstResult(offset); + } + if (ClassUtils.getBooleanFromMap(DynamicFinder.ARGUMENT_LOCK, argMap)) { + c.setLockMode(LockMode.PESSIMISTIC_WRITE); + c.setCacheable(false); + } else { + if (argMap.containsKey(DynamicFinder.ARGUMENT_CACHE)) { + c.setCacheable(ClassUtils.getBooleanFromMap(DynamicFinder.ARGUMENT_CACHE, argMap)); + } else { + cacheCriteriaByMapping(entity.getJavaClass(), c); + } + } + + final Object sortObj = argMap.get(DynamicFinder.ARGUMENT_SORT); + if (sortObj != null) { + boolean ignoreCase = true; + Object caseArg = argMap.get(DynamicFinder.ARGUMENT_IGNORE_CASE); + if (caseArg instanceof Boolean) { + ignoreCase = (Boolean) caseArg; + } + if (sortObj instanceof Map) { + Map sortMap = (Map) sortObj; + for (Object sort : sortMap.keySet()) { + final String order = DynamicFinder.ORDER_DESC.equalsIgnoreCase((String) sortMap.get(sort)) ? DynamicFinder.ORDER_DESC : DynamicFinder.ORDER_ASC; + addOrderPossiblyNested(c, entity, (String) sort, order, ignoreCase); + } + } else { + final String sort = (String) sortObj; + final String order = DynamicFinder.ORDER_DESC.equalsIgnoreCase(orderParam) ? DynamicFinder.ORDER_DESC : DynamicFinder.ORDER_ASC; + addOrderPossiblyNested(c, entity, sort, order, ignoreCase); + } + } else if (useDefaultMapping) { + Mapping m = AbstractGrailsDomainBinder.getMapping(entity.getJavaClass()); + if (m != null) { + Map sortMap = m.getSort().getNamesAndDirections(); + for (Object sort : sortMap.keySet()) { + final String order = DynamicFinder.ORDER_DESC.equalsIgnoreCase((String) sortMap.get(sort)) ? DynamicFinder.ORDER_DESC : DynamicFinder.ORDER_ASC; + addOrderPossiblyNested(c, entity, (String) sort, order, true); + } + } + } + } + + /** + * Populates criteria arguments for the given target class and arguments map + * + * @param entity The {@link org.grails.datastore.mapping.model.PersistentEntity} instance + * @param query The criteria instance + * @param argMap The arguments map + */ + @SuppressWarnings("rawtypes") + public static void populateArgumentsForCriteria( + PersistentEntity entity, + CriteriaQuery query, + Root queryRoot, + CriteriaBuilder criteriaBuilder, + Map argMap, + ConversionService conversionService, + boolean useDefaultMapping) { + String orderParam = (String) argMap.get(DynamicFinder.ARGUMENT_ORDER); + Object fetchObj = argMap.get(DynamicFinder.ARGUMENT_FETCH); + if (fetchObj instanceof Map) { + Map fetch = (Map) fetchObj; + for (Object o : fetch.keySet()) { + String associationName = (String) o; + + final FetchMode fetchMode = getFetchMode(fetch.get(associationName)); + if (fetchMode == FetchMode.JOIN) { + queryRoot.join(associationName); + } + } + } + + final Object sortObj = argMap.get(DynamicFinder.ARGUMENT_SORT); + if (sortObj != null) { + boolean ignoreCase = true; + Object caseArg = argMap.get(DynamicFinder.ARGUMENT_IGNORE_CASE); + if (caseArg instanceof Boolean) { + ignoreCase = (Boolean) caseArg; + } + if (sortObj instanceof Map) { + Map sortMap = (Map) sortObj; + for (Object sort : sortMap.keySet()) { + final String order = DynamicFinder.ORDER_DESC.equalsIgnoreCase((String) sortMap.get(sort)) ? DynamicFinder.ORDER_DESC : DynamicFinder.ORDER_ASC; + addOrderPossiblyNested(query, queryRoot, criteriaBuilder, entity, (String) sort, order, ignoreCase); + } + } else { + final String sort = (String) sortObj; + final String order = DynamicFinder.ORDER_DESC.equalsIgnoreCase(orderParam) ? DynamicFinder.ORDER_DESC : DynamicFinder.ORDER_ASC; + addOrderPossiblyNested(query, queryRoot, criteriaBuilder, entity, sort, order, ignoreCase); + } + } else if (useDefaultMapping) { + Mapping m = AbstractGrailsDomainBinder.getMapping(entity.getJavaClass()); + if (m != null) { + Map sortMap = m.getSort().getNamesAndDirections(); + for (Object sort : sortMap.keySet()) { + final String order = DynamicFinder.ORDER_DESC.equalsIgnoreCase((String) sortMap.get(sort)) ? DynamicFinder.ORDER_DESC : DynamicFinder.ORDER_ASC; + addOrderPossiblyNested(query, queryRoot, criteriaBuilder, entity, (String) sort, order, true); + } + } + } + } + + /** + * Populates criteria arguments for the given target class and arguments map + * + * @param entity The {@link org.grails.datastore.mapping.model.PersistentEntity} instance + * @param query The criteria instance + * @param argMap The arguments map + */ + @SuppressWarnings("rawtypes") + public static void populateArgumentsForCriteria( + PersistentEntity entity, + Query query, + Map argMap, + ConversionService conversionService, + boolean useDefaultMapping) { + Integer maxParam = null; + Integer offsetParam = null; + if (argMap.containsKey(DynamicFinder.ARGUMENT_MAX)) { + maxParam = conversionService.convert(argMap.get(DynamicFinder.ARGUMENT_MAX), Integer.class); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_OFFSET)) { + offsetParam = conversionService.convert(argMap.get(DynamicFinder.ARGUMENT_OFFSET), Integer.class); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_FETCH_SIZE)) { + query.setFetchSize(conversionService.convert(argMap.get(DynamicFinder.ARGUMENT_FETCH_SIZE), Integer.class)); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_TIMEOUT)) { + query.setTimeout(conversionService.convert(argMap.get(DynamicFinder.ARGUMENT_TIMEOUT), Integer.class)); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_FLUSH_MODE)) { + query.setHibernateFlushMode(convertFlushMode(argMap.get(DynamicFinder.ARGUMENT_FLUSH_MODE))); + } + if (argMap.containsKey(DynamicFinder.ARGUMENT_READ_ONLY)) { + query.setReadOnly(ClassUtils.getBooleanFromMap(DynamicFinder.ARGUMENT_READ_ONLY, argMap)); + } + + final int max = maxParam == null ? -1 : maxParam; + final int offset = offsetParam == null ? -1 : offsetParam; + if (max > -1) { + query.setMaxResults(max); + } + if (offset > -1) { + query.setFirstResult(offset); + } + if (ClassUtils.getBooleanFromMap(DynamicFinder.ARGUMENT_LOCK, argMap)) { + query.setLockMode(LockModeType.PESSIMISTIC_WRITE); + query.setCacheable(false); + } else { + if (argMap.containsKey(DynamicFinder.ARGUMENT_CACHE)) { + query.setCacheable(ClassUtils.getBooleanFromMap(DynamicFinder.ARGUMENT_CACHE, argMap)); + } else { + cacheCriteriaByMapping(entity.getJavaClass(), query); + } + } + + } + + /** + * Add order to criteria, creating necessary subCriteria if nested sort property (ie. sort:'nested.property'). + */ + private static void addOrderPossiblyNested(Criteria c, PersistentEntity entity, String sort, String order, boolean ignoreCase) { + int firstDotPos = sort.indexOf("."); + if (firstDotPos == -1) { + addOrder(c, sort, order, ignoreCase); + } else { // nested property + String sortHead = sort.substring(0, firstDotPos); + String sortTail = sort.substring(firstDotPos + 1); + PersistentProperty property = entity.getPropertyByName(sortHead); + if (property instanceof Embedded) { + // embedded objects cannot reference entities (at time of writing), so no more recursion needed + addOrder(c, sort, order, ignoreCase); + } else if (property instanceof Association) { + Association a = (Association) property; + Criteria subCriteria = c.createCriteria(sortHead); + PersistentEntity associatedEntity = a.getAssociatedEntity(); + Class propertyTargetClass = associatedEntity.getJavaClass(); + cacheCriteriaByMapping(propertyTargetClass, subCriteria); + addOrderPossiblyNested(subCriteria, associatedEntity, sortTail, order, ignoreCase); // Recurse on nested sort + } + } + } + + /** + * Add order to criteria, creating necessary subCriteria if nested sort property (ie. sort:'nested.property'). + */ + private static void addOrderPossiblyNested(CriteriaQuery query, + From queryRoot, + CriteriaBuilder criteriaBuilder, + PersistentEntity entity, + String sort, + String order, + boolean ignoreCase) { + int firstDotPos = sort.indexOf("."); + if (firstDotPos == -1) { + final PersistentProperty property = entity.getPropertyByName(sort); + ignoreCase = isIgnoreCaseProperty(ignoreCase, property); + addOrder(entity, query, queryRoot, criteriaBuilder, sort, order, ignoreCase); + } else { // nested property + String sortHead = sort.substring(0, firstDotPos); + String sortTail = sort.substring(firstDotPos + 1); + final PersistentProperty property = entity.getPropertyByName(sortHead); + if (property instanceof Embedded) { + // embedded objects cannot reference entities (at time of writing), so no more recursion needed + final PersistentProperty associatedProperty = ((Embedded) property).getAssociatedEntity().getPropertyByName(sortTail); + ignoreCase = isIgnoreCaseProperty(ignoreCase, associatedProperty); + addOrder(entity, query, queryRoot, criteriaBuilder, sort, order, ignoreCase); + } else if (property instanceof Association) { + final Association a = (Association) property; + final Join join = queryRoot.join(sortHead); + PersistentEntity associatedEntity = a.getAssociatedEntity(); + Class propertyTargetClass = associatedEntity.getJavaClass(); + addOrderPossiblyNested(query, join, criteriaBuilder, associatedEntity, sortTail, order, ignoreCase); // Recurse on nested sort + } + } + } + + private static boolean isIgnoreCaseProperty(boolean ignoreCase, PersistentProperty persistentProperty) { + if (ignoreCase && persistentProperty != null && persistentProperty.getType() != String.class) { + ignoreCase = false; + } + return ignoreCase; + } + + /** + * Add order directly to criteria. + */ + private static void addOrder(PersistentEntity entity, + CriteriaQuery query, + From queryRoot, + CriteriaBuilder criteriaBuilder, + String sort, String order, boolean ignoreCase) { + if (sort.equalsIgnoreCase(entity.getIdentity().getName())) { + Expression path = queryRoot; + + if (ignoreCase) { + path = criteriaBuilder.upper(path); + } + if (DynamicFinder.ORDER_DESC.equals(order)) { + query.orderBy(criteriaBuilder.desc(path)); + } else { + query.orderBy(criteriaBuilder.asc(path)); + } + } else { + Expression path = queryRoot.get(sort); + + if (ignoreCase) { + path = criteriaBuilder.upper(path); + } + if (DynamicFinder.ORDER_DESC.equals(order)) { + query.orderBy(criteriaBuilder.desc(path)); + } else { + query.orderBy(criteriaBuilder.asc(path)); + } + } + } + + /** + * Configures the criteria instance to cache based on the configured mapping. + * + * @param targetClass The target class + * @param criteria The criteria + */ + private static void cacheCriteriaByMapping(Class targetClass, Criteria criteria) { + Mapping m = AbstractGrailsDomainBinder.getMapping(targetClass); + if (m != null && m.getCache() != null && m.getCache().getEnabled()) { + criteria.setCacheable(true); + } + } + + /** + * Configures the criteria instance to cache based on the configured mapping. + * + * @param targetClass The target class + * @param criteria The criteria + */ + private static void cacheCriteriaByMapping(Class targetClass, Query criteria) { + Mapping m = AbstractGrailsDomainBinder.getMapping(targetClass); + if (m != null && m.getCache() != null && m.getCache().getEnabled()) { + criteria.setCacheable(true); + } + } + + private static FlushMode convertFlushMode(Object object) { + if (object == null) { + return null; + } + if (object instanceof FlushMode) { + return (FlushMode) object; + } + try { + return FlushMode.valueOf(object.toString()); + } catch (IllegalArgumentException e) { + return FlushMode.COMMIT; + } + } + + /** + * Add order directly to criteria. + */ + private static void addOrder(Criteria c, String sort, String order, boolean ignoreCase) { + if (DynamicFinder.ORDER_DESC.equals(order)) { + c.addOrder(ignoreCase ? Order.desc(sort).ignoreCase() : Order.desc(sort)); + } else { + c.addOrder(ignoreCase ? Order.asc(sort).ignoreCase() : Order.asc(sort)); + } + } + + /** + * Retrieves the fetch mode for the specified instance; otherwise returns the default FetchMode. + * + * @param object The object, converted to a string + * @return The FetchMode + */ + public static FetchMode getFetchMode(Object object) { + String name = object != null ? object.toString() : "default"; + if (name.equalsIgnoreCase(FetchMode.JOIN.toString()) || name.equalsIgnoreCase("eager")) { + return FetchMode.JOIN; + } + if (name.equalsIgnoreCase(FetchMode.SELECT.toString()) || name.equalsIgnoreCase("lazy")) { + return FetchMode.SELECT; + } + return FetchMode.DEFAULT; + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateCriterionAdapter.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateCriterionAdapter.java new file mode 100644 index 00000000000..ec2edab7c7b --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateCriterionAdapter.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.query; + +import org.hibernate.criterion.Criterion; +import org.hibernate.criterion.DetachedCriteria; + +import grails.orm.HibernateCriteriaBuilder; +import grails.orm.RlikeExpression; +import org.grails.datastore.mapping.query.api.QueryableCriteria; + +/** + * @author Graeme Rocher + * @since 2.0 + */ +public class HibernateCriterionAdapter extends AbstractHibernateCriterionAdapter { + + protected Criterion createRlikeExpression(String propertyName, String pattern) { + return new RlikeExpression(propertyName, pattern); + } + + @Override + protected DetachedCriteria toHibernateDetachedCriteria(AbstractHibernateQuery hibernateQuery, QueryableCriteria queryableCriteria) { + return HibernateCriteriaBuilder.getHibernateDetachedCriteria(hibernateQuery, queryableCriteria); + } + + @Override + protected DetachedCriteria toHibernateDetachedCriteria(AbstractHibernateQuery hibernateQuery, QueryableCriteria queryableCriteria, String alias) { + if (alias == null) { + return toHibernateDetachedCriteria(hibernateQuery, queryableCriteria); + } + return HibernateCriteriaBuilder.getHibernateDetachedCriteria(hibernateQuery, queryableCriteria, alias); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java new file mode 100644 index 00000000000..3bf5c1b01a9 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.query; + +import java.util.List; + +import org.springframework.context.ApplicationEventPublisher; + +import org.grails.datastore.mapping.core.Datastore; +import org.grails.datastore.mapping.core.Session; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.query.Query; +import org.grails.datastore.mapping.query.event.PostQueryEvent; +import org.grails.datastore.mapping.query.event.PreQueryEvent; + +/** + * A query implementation for HQL queries + * + * @author Graeme Rocher + * @since 6.0 + */ +public class HibernateHqlQuery extends Query { + private final org.hibernate.query.Query query; + + public HibernateHqlQuery(Session session, PersistentEntity entity, org.hibernate.query.Query query) { + super(session, entity); + this.query = query; + } + + @Override + protected void flushBeforeQuery() { + // do nothing, hibernate handles this + } + + @Override + protected List executeQuery(PersistentEntity entity, Junction criteria) { + Datastore datastore = getSession().getDatastore(); + ApplicationEventPublisher applicationEventPublisher = datastore.getApplicationEventPublisher(); + PreQueryEvent preQueryEvent = new PreQueryEvent(datastore, this); + applicationEventPublisher.publishEvent(preQueryEvent); + + if (uniqueResult) { + query.setMaxResults(1); + List results = query.list(); + applicationEventPublisher.publishEvent(new PostQueryEvent(datastore, this, results)); + return results; + } + else { + + List results = query.list(); + applicationEventPublisher.publishEvent(new PostQueryEvent(datastore, this, results)); + return results; + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateProjectionAdapter.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateProjectionAdapter.java new file mode 100644 index 00000000000..d296b01d038 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateProjectionAdapter.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.query; + +import java.util.HashMap; +import java.util.Map; + +import org.hibernate.criterion.Projection; +import org.hibernate.criterion.Projections; + +import org.grails.datastore.mapping.query.Query; + +/** + * Adapts Grails datastore API to Hibernate projections. + * + * @author Graeme Rocher + * @since 2.0 + */ +public class HibernateProjectionAdapter { + private Query.Projection projection; + private static Map, ProjectionAdapter> adapterMap = new HashMap<>(); + + static { + adapterMap.put(Query.AvgProjection.class, gormProjection -> { + Query.AvgProjection avg = (Query.AvgProjection) gormProjection; + return Projections.avg(avg.getPropertyName()); + }); + adapterMap.put(Query.IdProjection.class, gormProjection -> Projections.id()); + adapterMap.put(Query.SumProjection.class, gormProjection -> { + Query.SumProjection avg = (Query.SumProjection) gormProjection; + return Projections.sum(avg.getPropertyName()); + }); + adapterMap.put(Query.DistinctPropertyProjection.class, gormProjection -> { + Query.DistinctPropertyProjection avg = (Query.DistinctPropertyProjection) gormProjection; + return Projections.distinct(Projections.property(avg.getPropertyName())); + }); + adapterMap.put(Query.PropertyProjection.class, gormProjection -> { + Query.PropertyProjection avg = (Query.PropertyProjection) gormProjection; + return Projections.property(avg.getPropertyName()); + }); + adapterMap.put(Query.CountProjection.class, gormProjection -> Projections.rowCount()); + adapterMap.put(Query.CountDistinctProjection.class, gormProjection -> { + Query.CountDistinctProjection cd = (Query.CountDistinctProjection) gormProjection; + return Projections.countDistinct(cd.getPropertyName()); + }); + adapterMap.put(Query.GroupPropertyProjection.class, gormProjection -> { + Query.GroupPropertyProjection cd = (Query.GroupPropertyProjection) gormProjection; + return Projections.groupProperty(cd.getPropertyName()); + }); + adapterMap.put(Query.MaxProjection.class, gormProjection -> { + Query.MaxProjection cd = (Query.MaxProjection) gormProjection; + return Projections.max(cd.getPropertyName()); + }); + adapterMap.put(Query.MinProjection.class, gormProjection -> { + Query.MinProjection cd = (Query.MinProjection) gormProjection; + return Projections.min(cd.getPropertyName()); + }); + } + + public HibernateProjectionAdapter(Query.Projection projection) { + this.projection = projection; + } + + public Projection toHibernateProjection() { + ProjectionAdapter projectionAdapter = adapterMap.get(projection.getClass()); + if (projectionAdapter == null) throw new UnsupportedOperationException("Unsupported projection used: " + projection.getClass().getName()); + return projectionAdapter.toHibernateProjection(projection); + } + + private interface ProjectionAdapter { + Projection toHibernateProjection(Query.Projection gormProjection); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java new file mode 100644 index 00000000000..8d016e2e760 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.query; + +import java.util.Iterator; +import java.util.List; + +import org.hibernate.Criteria; +import org.hibernate.SessionFactory; +import org.hibernate.criterion.DetachedCriteria; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.function.SQLFunction; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.CriteriaImpl; +import org.hibernate.persister.entity.PropertyMapping; +import org.hibernate.type.BasicType; +import org.hibernate.type.TypeResolver; + +import grails.orm.HibernateCriteriaBuilder; +import grails.orm.RlikeExpression; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.query.api.QueryableCriteria; +import org.grails.orm.hibernate.AbstractHibernateSession; +import org.grails.orm.hibernate.GrailsHibernateTemplate; +import org.grails.orm.hibernate.HibernateSession; + +/** + * Bridges the Query API with the Hibernate Criteria API + * + * @author Graeme Rocher + * @since 1.0 + */ +@SuppressWarnings("rawtypes") +public class HibernateQuery extends AbstractHibernateQuery { + + public static final HibernateCriterionAdapter HIBERNATE_CRITERION_ADAPTER = new HibernateCriterionAdapter(); + + public HibernateQuery(Criteria criteria, AbstractHibernateSession session, PersistentEntity entity) { + super(criteria, session, entity); + } + + public HibernateQuery(Criteria criteria, PersistentEntity entity) { + super(criteria, null, entity); + } + + public HibernateQuery(Criteria subCriteria, AbstractHibernateSession session, PersistentEntity associatedEntity, String newAlias) { + super(subCriteria, session, associatedEntity, newAlias); + } + + public HibernateQuery(DetachedCriteria criteria, PersistentEntity entity) { + super(criteria, entity); + } + + /** + * @return The hibernate criteria + */ + public Criteria getHibernateCriteria() { + return this.criteria; + } + + @Override + protected AbstractHibernateCriterionAdapter createHibernateCriterionAdapter() { + return HIBERNATE_CRITERION_ADAPTER; + } + + protected org.hibernate.criterion.Criterion createRlikeExpression(String propertyName, String value) { + return new RlikeExpression(propertyName, value); + } + + protected void setDetachedCriteriaValue(QueryableCriteria value, PropertyCriterion pc) { + DetachedCriteria hibernateDetachedCriteria = HibernateCriteriaBuilder.getHibernateDetachedCriteria(this, value); + pc.setValue(hibernateDetachedCriteria); + } + + protected String render(BasicType basic, List columns, SessionFactory sessionFactory, SQLFunction sqlFunction) { + return sqlFunction.render(basic, columns, (SessionFactoryImplementor) sessionFactory); + } + + protected PropertyMapping getEntityPersister(String name, SessionFactory sessionFactory) { + return (PropertyMapping) ((SessionFactoryImplementor) sessionFactory).getEntityPersister(name); + } + + @Deprecated + protected TypeResolver getTypeResolver(SessionFactory sessionFactory) { + return ((SessionFactoryImplementor) sessionFactory).getTypeResolver(); + } + + @Deprecated + protected Dialect getDialect(SessionFactory sessionFactory) { + return ((SessionFactoryImplementor) sessionFactory).getDialect(); + } + + @Override + public Object clone() { + final CriteriaImpl impl = (CriteriaImpl) criteria; + final HibernateSession hibernateSession = (HibernateSession) getSession(); + final GrailsHibernateTemplate hibernateTemplate = (GrailsHibernateTemplate) hibernateSession.getNativeInterface(); + return hibernateTemplate.execute((GrailsHibernateTemplate.HibernateCallback) session -> { + Criteria newCriteria = session.createCriteria(impl.getEntityOrClassName()); + + Iterator iterator = impl.iterateExpressionEntries(); + while (iterator.hasNext()) { + CriteriaImpl.CriterionEntry entry = (CriteriaImpl.CriterionEntry) iterator.next(); + newCriteria.add(entry.getCriterion()); + } + Iterator subcriteriaIterator = impl.iterateSubcriteria(); + while (subcriteriaIterator.hasNext()) { + CriteriaImpl.Subcriteria sub = (CriteriaImpl.Subcriteria) subcriteriaIterator.next(); + newCriteria.createAlias(sub.getPath(), sub.getAlias(), sub.getJoinType(), sub.getWithClause()); + } + return new HibernateQuery(newCriteria, hibernateSession, entity); + }); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQueryConstants.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQueryConstants.java new file mode 100644 index 00000000000..0f85cd090ed --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQueryConstants.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.query; + +/** + * Constants used for query arguments etc. + * + * @since 3.0.7 + * @author Graeme Rocher + */ +public interface HibernateQueryConstants { + + String ARGUMENT_FETCH_SIZE = "fetchSize"; + String ARGUMENT_TIMEOUT = "timeout"; + String ARGUMENT_READ_ONLY = "readOnly"; + String ARGUMENT_FLUSH_MODE = "flushMode"; + String ARGUMENT_MAX = "max"; + String ARGUMENT_OFFSET = "offset"; + String ARGUMENT_ORDER = "order"; + String ARGUMENT_SORT = "sort"; + String ORDER_DESC = "desc"; + String ORDER_ASC = "asc"; + String ARGUMENT_FETCH = "fetch"; + String ARGUMENT_IGNORE_CASE = "ignoreCase"; + String ARGUMENT_CACHE = "cache"; + String ARGUMENT_LOCK = "lock"; + String CONFIG_PROPERTY_CACHE_QUERIES = "grails.hibernate.cache.queries"; + String CONFIG_PROPERTY_OSIV_READONLY = "grails.hibernate.osiv.readonly"; + String CONFIG_PROPERTY_PASS_READONLY_TO_HIBERNATE = "grails.hibernate.pass.readonly"; +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java new file mode 100644 index 00000000000..e1add6d6c26 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.query; + +import java.sql.SQLException; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.query.Query; + +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.orm.hibernate.GrailsHibernateTemplate; + +public class PagedResultList extends grails.gorm.PagedResultList { + + private final CriteriaQuery criteriaQuery; + private final Root queryRoot; + private final CriteriaBuilder criteriaBuilder; + private final PersistentEntity entity; + private transient GrailsHibernateTemplate hibernateTemplate; + + public PagedResultList(GrailsHibernateTemplate template, + PersistentEntity entity, + HibernateHqlQuery hibernateHqlQuery, + CriteriaQuery criteriaQuery, + Root queryRoot, + CriteriaBuilder criteriaBuilder) { + super(hibernateHqlQuery); + hibernateTemplate = template; + this.criteriaQuery = criteriaQuery; + this.queryRoot = queryRoot; + this.criteriaBuilder = criteriaBuilder; + this.entity = entity; + } + + @Override + protected void initialize() { + // no-op, already initialized + } + + @Override + public int getTotalCount() { + if (totalCount == Integer.MIN_VALUE) { + totalCount = hibernateTemplate.execute(new GrailsHibernateTemplate.HibernateCallback<>() { + public Integer doInHibernate(Session session) throws HibernateException, SQLException { + final CriteriaQuery finalQuery = criteriaQuery.select(criteriaBuilder.count(queryRoot)).distinct(true).orderBy(); + final Query query = session.createQuery(finalQuery); + hibernateTemplate.applySettings(query); + return ((Number) query.uniqueResult()).intValue(); + } + }); + } + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/AbstractClosureEventTriggeringInterceptor.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/AbstractClosureEventTriggeringInterceptor.java new file mode 100644 index 00000000000..c395e9bd0a2 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/AbstractClosureEventTriggeringInterceptor.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.support; + +import org.hibernate.event.internal.DefaultSaveOrUpdateEventListener; +import org.hibernate.event.spi.PostDeleteEventListener; +import org.hibernate.event.spi.PostInsertEventListener; +import org.hibernate.event.spi.PostLoadEventListener; +import org.hibernate.event.spi.PostUpdateEventListener; +import org.hibernate.event.spi.PreDeleteEventListener; +import org.hibernate.event.spi.PreInsertEventListener; +import org.hibernate.event.spi.PreLoadEventListener; +import org.hibernate.event.spi.PreUpdateEventListener; + +import org.springframework.context.ApplicationContextAware; + +/** + * Abstract class for defining the event triggering interceptor + * + * @author Graeme Rocher + * @since 6.0 + */ +public abstract class AbstractClosureEventTriggeringInterceptor extends DefaultSaveOrUpdateEventListener + implements ApplicationContextAware, + PreLoadEventListener, + PostLoadEventListener, + PostInsertEventListener, + PostUpdateEventListener, + PostDeleteEventListener, + PreDeleteEventListener, + PreUpdateEventListener, + PreInsertEventListener { +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java new file mode 100644 index 00000000000..fe0f31e1597 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java @@ -0,0 +1,390 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.support; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import groovy.lang.Closure; +import groovy.lang.GroovySystem; +import groovy.lang.MetaClass; + +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.action.internal.EntityUpdateAction; +import org.hibernate.engine.spi.ActionQueue; +import org.hibernate.engine.spi.ExecutableList; +import org.hibernate.event.spi.AbstractEvent; +import org.hibernate.event.spi.AbstractPreDatabaseOperationEvent; +import org.hibernate.event.spi.PostDeleteEvent; +import org.hibernate.event.spi.PostDeleteEventListener; +import org.hibernate.event.spi.PostInsertEvent; +import org.hibernate.event.spi.PostInsertEventListener; +import org.hibernate.event.spi.PostLoadEvent; +import org.hibernate.event.spi.PostLoadEventListener; +import org.hibernate.event.spi.PostUpdateEvent; +import org.hibernate.event.spi.PostUpdateEventListener; +import org.hibernate.event.spi.PreDeleteEvent; +import org.hibernate.event.spi.PreDeleteEventListener; +import org.hibernate.event.spi.PreInsertEvent; +import org.hibernate.event.spi.PreLoadEvent; +import org.hibernate.event.spi.PreLoadEventListener; +import org.hibernate.event.spi.PreUpdateEvent; +import org.hibernate.event.spi.PreUpdateEventListener; +import org.hibernate.event.spi.SaveOrUpdateEvent; +import org.hibernate.event.spi.SaveOrUpdateEventListener; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.tuple.entity.EntityMetamodel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.util.ReflectionUtils; +import org.springframework.validation.Errors; + +import org.grails.datastore.gorm.GormValidateable; +import org.grails.datastore.gorm.support.BeforeValidateHelper.BeforeValidateEventTriggerCaller; +import org.grails.datastore.gorm.support.EventTriggerCaller; +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent; +import org.grails.datastore.mapping.engine.event.ValidationEvent; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.config.GormProperties; +import org.grails.datastore.mapping.reflect.ClassUtils; +import org.grails.datastore.mapping.reflect.EntityReflector; +import org.grails.datastore.mapping.validation.ValidationException; +import org.grails.orm.hibernate.AbstractHibernateGormValidationApi; + +/** + *

Invokes closure events on domain entities such as beforeInsert, beforeUpdate and beforeDelete. + * + *

Also deals with auto time stamping of domain classes that have properties named 'lastUpdated' and/or 'dateCreated'. + * + * @author Lari Hotari + * @author Graeme Rocher + * @since 1.3.5 + */ +@SuppressWarnings({"rawtypes", "unchecked", "serial"}) +public class ClosureEventListener implements SaveOrUpdateEventListener, + PreLoadEventListener, + PostLoadEventListener, + PostInsertEventListener, + PostUpdateEventListener, + PostDeleteEventListener, + PreDeleteEventListener, + PreUpdateEventListener { + + private static final long serialVersionUID = 1; + protected static final Logger LOG = LoggerFactory.getLogger(ClosureEventListener.class); + + private final EventTriggerCaller saveOrUpdateCaller; + private final EventTriggerCaller beforeInsertCaller; + private final EventTriggerCaller preLoadEventCaller; + private final EventTriggerCaller postLoadEventListener; + private final EventTriggerCaller postInsertEventListener; + private final EventTriggerCaller postUpdateEventListener; + private final EventTriggerCaller postDeleteEventListener; + private final EventTriggerCaller preDeleteEventListener; + private final EventTriggerCaller preUpdateEventListener; + private final BeforeValidateEventTriggerCaller beforeValidateEventListener; + private final PersistentEntity persistentEntity; + private final MetaClass domainMetaClass; + private final boolean isMultiTenant; + private final boolean failOnErrorEnabled; + private final Map validateParams; + + private Field actionQueueUpdatesField; + private Field entityUpdateActionStateField; + + public ClosureEventListener(PersistentEntity persistentEntity, boolean failOnError, List failOnErrorPackages) { + this.persistentEntity = persistentEntity; + Class domainClazz = persistentEntity.getJavaClass(); + this.domainMetaClass = GroovySystem.getMetaClassRegistry().getMetaClass(domainClazz); + this.isMultiTenant = ClassUtils.isMultiTenant(domainClazz); + saveOrUpdateCaller = buildCaller(AbstractPersistenceEvent.ONLOAD_SAVE, domainClazz); + beforeInsertCaller = buildCaller(AbstractPersistenceEvent.BEFORE_INSERT_EVENT, domainClazz); + EventTriggerCaller preLoadEventCaller = buildCaller(AbstractPersistenceEvent.ONLOAD_EVENT, domainClazz); + if (preLoadEventCaller == null) { + this.preLoadEventCaller = buildCaller(AbstractPersistenceEvent.BEFORE_LOAD_EVENT, domainClazz); + } + else { + this.preLoadEventCaller = preLoadEventCaller; + } + + postLoadEventListener = buildCaller(AbstractPersistenceEvent.AFTER_LOAD_EVENT, domainClazz); + postInsertEventListener = buildCaller(AbstractPersistenceEvent.AFTER_INSERT_EVENT, domainClazz); + postUpdateEventListener = buildCaller(AbstractPersistenceEvent.AFTER_UPDATE_EVENT, domainClazz); + postDeleteEventListener = buildCaller(AbstractPersistenceEvent.AFTER_DELETE_EVENT, domainClazz); + preDeleteEventListener = buildCaller(AbstractPersistenceEvent.BEFORE_DELETE_EVENT, domainClazz); + preUpdateEventListener = buildCaller(AbstractPersistenceEvent.BEFORE_UPDATE_EVENT, domainClazz); + + beforeValidateEventListener = new BeforeValidateEventTriggerCaller(domainClazz, domainMetaClass); + + if (failOnErrorPackages.size() > 0) { + failOnErrorEnabled = ClassUtils.isClassBelowPackage(domainClazz, failOnErrorPackages); + } else { + failOnErrorEnabled = failOnError; + } + + validateParams = new HashMap(); + validateParams.put(AbstractHibernateGormValidationApi.ARGUMENT_DEEP_VALIDATE, Boolean.FALSE); + + try { + actionQueueUpdatesField = ReflectionUtils.findField(ActionQueue.class, "updates"); + actionQueueUpdatesField.setAccessible(true); + entityUpdateActionStateField = ReflectionUtils.findField(EntityUpdateAction.class, "state"); + entityUpdateActionStateField.setAccessible(true); + } catch (Exception e) { + // ignore + } + } + + public void onSaveOrUpdate(SaveOrUpdateEvent event) throws HibernateException { + // no-op, merely a hook for plugins to override + } + + public void onPreLoad(final PreLoadEvent event) { + if (preLoadEventCaller == null) { + return; + } + + doWithManualSession(event, new Closure(this) { + @Override + public Object call() { + preLoadEventCaller.call(event.getEntity()); + return null; + } + }); + } + + public void onPostLoad(final PostLoadEvent event) { + if (postLoadEventListener == null) { + return; + } + + doWithManualSession(event, new Closure(this) { + @Override + public Object call() { + postLoadEventListener.call(event.getEntity()); + return null; + } + }); + } + + public void onPostInsert(PostInsertEvent event) { + final Object entity = event.getEntity(); + if (postInsertEventListener == null) { + return; + } + + doWithManualSession(event, new Closure(this) { + @Override + public Object call() { + postInsertEventListener.call(entity); + return null; + } + }); + } + + @Override + public boolean requiresPostCommitHanding(EntityPersister persister) { + return false; + } + + @Override + public boolean requiresPostCommitHandling(EntityPersister persister) { + return false; + } + + public void onPostUpdate(PostUpdateEvent event) { + final Object entity = event.getEntity(); + if (postUpdateEventListener == null) { + return; + } + + doWithManualSession(event, new Closure(this) { + @Override + public Object call() { + postUpdateEventListener.call(entity); + return null; + } + }); + } + + public void onPostDelete(PostDeleteEvent event) { + final Object entity = event.getEntity(); + if (postDeleteEventListener == null) { + return; + } + + doWithManualSession(event, new Closure(this) { + @Override + public Object call() { + postDeleteEventListener.call(entity); + return null; + } + }); + } + + public boolean onPreDelete(final PreDeleteEvent event) { + if (preDeleteEventListener == null) { + return false; + } + + return doWithManualSession(event, new Closure<>(this) { + @Override + public Boolean call() { + return preDeleteEventListener.call(event.getEntity()); + } + }); + } + + public boolean onPreUpdate(final PreUpdateEvent event) { + return doWithManualSession(event, new Closure<>(this) { + @Override + public Boolean call() { + Object entity = event.getEntity(); + boolean evict = false; + if (preUpdateEventListener != null) { + evict = preUpdateEventListener.call(entity); + if (!evict) { + synchronizePersisterState(event, event.getState()); + } + } + return evict || doValidate(entity); + } + }); + } + + public boolean onPreInsert(final PreInsertEvent event) { + return doWithManualSession(event, new Closure<>(this) { + @Override + public Boolean call() { + Object entity = event.getEntity(); + boolean synchronizeState = false; + if (beforeInsertCaller != null) { + if (beforeInsertCaller.call(entity)) { + return true; + } + synchronizeState = true; + } + if (synchronizeState) { + synchronizePersisterState(event, event.getState()); + } + return doValidate(entity); + } + + }); + } + + public void onValidate(ValidationEvent event) { + beforeValidateEventListener.call(event.getEntityObject(), event.getValidatedFields()); + } + + protected boolean doValidate(Object entity) { + boolean evict = false; + GormValidateable validateable = (GormValidateable) entity; + if (!validateable.shouldSkipValidation() && + !validateable.validate(validateParams)) { + evict = true; + if (failOnErrorEnabled) { + Errors errors = validateable.getErrors(); + throw ValidationException.newInstance("Validation error whilst flushing entity [" + entity.getClass().getName() + + "]", errors); + } + } + return evict; + } + + private EventTriggerCaller buildCaller(String eventName, Class domainClazz) { + return EventTriggerCaller.buildCaller(eventName, domainClazz, domainMetaClass, null); + } + + private void synchronizePersisterState(AbstractPreDatabaseOperationEvent event, Object[] state) { + EntityPersister persister = event.getPersister(); + synchronizePersisterState(event, state, persister, persister.getPropertyNames()); + } + + private void synchronizePersisterState(AbstractPreDatabaseOperationEvent event, Object[] state, EntityPersister persister, String[] propertyNames) { + Object entity = event.getEntity(); + EntityReflector reflector = persistentEntity.getReflector(); + HashMap changedState = new HashMap<>(); + EntityMetamodel entityMetamodel = persister.getEntityMetamodel(); + for (int i = 0; i < propertyNames.length; i++) { + String p = propertyNames[i]; + Integer index = entityMetamodel.getPropertyIndexOrNull(p); + if (index == null) continue; + + PersistentProperty property = persistentEntity.getPropertyByName(p); + if (property == null) { + continue; + } + String propertyName = property.getName(); + + if (GormProperties.VERSION.equals(propertyName)) { + continue; + } + + Object value = reflector.getProperty(entity, propertyName); + if (state[index] != value) { + changedState.put(i, value); + } + state[index] = value; + } + + synchronizeEntityUpdateActionState(event, entity, changedState); + } + + private void synchronizeEntityUpdateActionState(AbstractPreDatabaseOperationEvent event, Object entity, + HashMap changedState) { + if (actionQueueUpdatesField != null && event instanceof PreInsertEvent && changedState.size() > 0) { + try { + ExecutableList updates = (ExecutableList) actionQueueUpdatesField.get(event.getSession().getActionQueue()); + if (updates != null) { + for (EntityUpdateAction updateAction : updates) { + if (updateAction.getInstance() == entity) { + Object[] updateState = (Object[]) entityUpdateActionStateField.get(updateAction); + if (updateState != null) { + for (Map.Entry entry : changedState.entrySet()) { + updateState[entry.getKey()] = entry.getValue(); + } + } + } + } + } + } + catch (Exception e) { + LOG.warn("Error synchronizing object state with Hibernate: " + e.getMessage(), e); + } + } + } + + private T doWithManualSession(AbstractEvent event, Closure callable) { + Session session = event.getSession(); + FlushMode current = session.getHibernateFlushMode(); + try { + session.setHibernateFlushMode(FlushMode.MANUAL); + return callable.call(); + } finally { + session.setHibernateFlushMode(current); + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java new file mode 100644 index 00000000000..422e12cbe6b --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.support; + +import java.util.Map; + +import org.hibernate.Hibernate; +import org.hibernate.HibernateException; +import org.hibernate.event.spi.AbstractEvent; +import org.hibernate.event.spi.PostDeleteEvent; +import org.hibernate.event.spi.PostInsertEvent; +import org.hibernate.event.spi.PostLoadEvent; +import org.hibernate.event.spi.PostUpdateEvent; +import org.hibernate.event.spi.PreDeleteEvent; +import org.hibernate.event.spi.PreInsertEvent; +import org.hibernate.event.spi.PreLoadEvent; +import org.hibernate.event.spi.PreUpdateEvent; +import org.hibernate.event.spi.SaveOrUpdateEvent; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.tuple.entity.EntityMetamodel; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +import org.grails.datastore.gorm.events.AutoTimestampEventListener; +import org.grails.datastore.gorm.events.ConfigurableApplicationContextEventPublisher; +import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher; +import org.grails.datastore.mapping.dirty.checking.DirtyCheckable; +import org.grails.datastore.mapping.engine.ModificationTrackingEntityAccess; +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent; +import org.grails.datastore.mapping.model.MappingContext; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.types.Embedded; +import org.grails.datastore.mapping.proxy.ProxyHandler; +import org.grails.orm.hibernate.AbstractHibernateDatastore; + +/** + * Listens for Hibernate events and publishes corresponding Datastore events. + * + * @author Graeme Rocher + * @author Lari Hotari + * @author Burt Beckwith + * @since 1.0 + */ +public class ClosureEventTriggeringInterceptor extends AbstractClosureEventTriggeringInterceptor { + + // private final Logger log = LoggerFactory.getLogger(getClass()); + private static final long serialVersionUID = 1; + + protected AbstractHibernateDatastore datastore; + protected ConfigurableApplicationEventPublisher eventPublisher; + + private MappingContext mappingContext; + private ProxyHandler proxyHandler; + + public void setDatastore(AbstractHibernateDatastore datastore) { + this.datastore = datastore; + this.mappingContext = datastore.getMappingContext(); + this.proxyHandler = mappingContext.getProxyHandler(); + } + + public void setEventPublisher(ConfigurableApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Override + public void onSaveOrUpdate(SaveOrUpdateEvent hibernateEvent) throws HibernateException { + Object entity = getEntity(hibernateEvent); + if (entity != null && proxyHandler.isInitialized(entity)) { + activateDirtyChecking(entity); + org.grails.datastore.mapping.engine.event.SaveOrUpdateEvent grailsEvent = new org.grails.datastore.mapping.engine.event.SaveOrUpdateEvent( + this.datastore, entity); + publishEvent(hibernateEvent, grailsEvent); + } + super.onSaveOrUpdate(hibernateEvent); + } + + protected Object getEntity(SaveOrUpdateEvent hibernateEvent) { + Object object = hibernateEvent.getObject(); + if (object != null) { + return object; + } + else { + return hibernateEvent.getEntity(); + } + } + + public void onPreLoad(PreLoadEvent hibernateEvent) { + org.grails.datastore.mapping.engine.event.PreLoadEvent grailsEvent = new org.grails.datastore.mapping.engine.event.PreLoadEvent( + this.datastore, hibernateEvent.getEntity()); + publishEvent(hibernateEvent, grailsEvent); + } + + public void onPostLoad(PostLoadEvent hibernateEvent) { + Object entity = hibernateEvent.getEntity(); + activateDirtyChecking(entity); + publishEvent(hibernateEvent, new org.grails.datastore.mapping.engine.event.PostLoadEvent( + this.datastore, entity)); + } + + public boolean onPreInsert(PreInsertEvent hibernateEvent) { + Object entity = hibernateEvent.getEntity(); + Class type = Hibernate.getClass(entity); + PersistentEntity persistentEntity = mappingContext.getPersistentEntity(type.getName()); + AbstractPersistenceEvent grailsEvent; + ModificationTrackingEntityAccess entityAccess = null; + if (persistentEntity != null) { + entityAccess = new ModificationTrackingEntityAccess(mappingContext.createEntityAccess(persistentEntity, entity)); + grailsEvent = new org.grails.datastore.mapping.engine.event.PreInsertEvent(this.datastore, persistentEntity, entityAccess); + } + else { + grailsEvent = new org.grails.datastore.mapping.engine.event.PreInsertEvent(this.datastore, entity); + } + + publishEvent(hibernateEvent, grailsEvent); + + boolean cancelled = grailsEvent.isCancelled(); + if (!cancelled && entityAccess != null) { + synchronizeHibernateState(hibernateEvent, entityAccess); + } + return cancelled; + } + + private void synchronizeHibernateState(PreInsertEvent hibernateEvent, ModificationTrackingEntityAccess entityAccess) { + Map modifiedProperties = entityAccess.getModifiedProperties(); + if (!modifiedProperties.isEmpty()) { + Object[] state = hibernateEvent.getState(); + EntityPersister persister = hibernateEvent.getPersister(); + synchronizeHibernateState(persister, state, modifiedProperties); + } + } + + private void synchronizeHibernateState(PreUpdateEvent hibernateEvent, ModificationTrackingEntityAccess entityAccess, boolean autoTimestamp) { + Map modifiedProperties = entityAccess.getModifiedProperties(); + + if (autoTimestamp) { + updateModifiedPropertiesWithAutoTimestamp(modifiedProperties, hibernateEvent); + } + + if (!modifiedProperties.isEmpty()) { + Object[] state = hibernateEvent.getState(); + EntityPersister persister = hibernateEvent.getPersister(); + synchronizeHibernateState(persister, state, modifiedProperties); + } + } + + private void updateModifiedPropertiesWithAutoTimestamp(Map modifiedProperties, PreUpdateEvent hibernateEvent) { + + EntityMetamodel entityMetamodel = hibernateEvent.getPersister().getEntityMetamodel(); + Integer dateCreatedIdx = entityMetamodel.getPropertyIndexOrNull(AutoTimestampEventListener.DATE_CREATED_PROPERTY); + + Object[] oldState = hibernateEvent.getOldState(); + Object[] state = hibernateEvent.getState(); + + // Only for "dateCreated" property, "lastUpdated" is handled correctly + if (dateCreatedIdx != null && oldState != null && oldState[dateCreatedIdx] != null && !oldState[dateCreatedIdx].equals(state[dateCreatedIdx])) { + modifiedProperties.put(AutoTimestampEventListener.DATE_CREATED_PROPERTY, oldState[dateCreatedIdx]); + } + } + + private void synchronizeHibernateState(EntityPersister persister, Object[] state, Map modifiedProperties) { + EntityMetamodel entityMetamodel = persister.getEntityMetamodel(); + for (Map.Entry entry : modifiedProperties.entrySet()) { + Integer index = entityMetamodel.getPropertyIndexOrNull(entry.getKey()); + if (index != null) { + state[index] = entry.getValue(); + } + } + } + + public void onPostInsert(PostInsertEvent hibernateEvent) { + Object entity = hibernateEvent.getEntity(); + org.grails.datastore.mapping.engine.event.PostInsertEvent grailsEvent = new org.grails.datastore.mapping.engine.event.PostInsertEvent( + this.datastore, entity); + activateDirtyChecking(entity); + publishEvent(hibernateEvent, grailsEvent); + } + + public boolean onPreUpdate(PreUpdateEvent hibernateEvent) { + Object entity = hibernateEvent.getEntity(); + Class type = Hibernate.getClass(entity); + MappingContext mappingContext = datastore.getMappingContext(); + PersistentEntity persistentEntity = mappingContext.getPersistentEntity(type.getName()); + AbstractPersistenceEvent grailsEvent; + ModificationTrackingEntityAccess entityAccess = null; + if (persistentEntity != null) { + entityAccess = new ModificationTrackingEntityAccess(mappingContext.createEntityAccess(persistentEntity, entity)); + grailsEvent = new org.grails.datastore.mapping.engine.event.PreUpdateEvent(this.datastore, persistentEntity, entityAccess); + } + else { + grailsEvent = new org.grails.datastore.mapping.engine.event.PreUpdateEvent(this.datastore, entity); + } + + publishEvent(hibernateEvent, grailsEvent); + boolean cancelled = grailsEvent.isCancelled(); + if (!cancelled && entityAccess != null) { + boolean autoTimestamp = persistentEntity.getMapping().getMappedForm().isAutoTimestamp(); + synchronizeHibernateState(hibernateEvent, entityAccess, autoTimestamp); + } + return cancelled; + + } + + public void onPostUpdate(PostUpdateEvent hibernateEvent) { + Object entity = hibernateEvent.getEntity(); + activateDirtyChecking(entity); + publishEvent(hibernateEvent, new org.grails.datastore.mapping.engine.event.PostUpdateEvent( + this.datastore, entity)); + } + + public boolean onPreDelete(PreDeleteEvent hibernateEvent) { + AbstractPersistenceEvent event = new org.grails.datastore.mapping.engine.event.PreDeleteEvent( + this.datastore, hibernateEvent.getEntity()); + publishEvent(hibernateEvent, event); + return event.isCancelled(); + } + + public void onPostDelete(PostDeleteEvent hibernateEvent) { + org.grails.datastore.mapping.engine.event.PostDeleteEvent grailsEvent = new org.grails.datastore.mapping.engine.event.PostDeleteEvent( + this.datastore, hibernateEvent.getEntity()); + publishEvent(hibernateEvent, grailsEvent); + } + + private void publishEvent(AbstractEvent hibernateEvent, AbstractPersistenceEvent mappingEvent) { + mappingEvent.setNativeEvent(hibernateEvent); + if (eventPublisher != null) { + eventPublisher.publishEvent(mappingEvent); + } + } + + @Override + public boolean requiresPostCommitHanding(EntityPersister persister) { + return false; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (applicationContext instanceof ConfigurableApplicationContext) { + + this.eventPublisher = new ConfigurableApplicationContextEventPublisher((ConfigurableApplicationContext) applicationContext); + } + } + + private void activateDirtyChecking(Object entity) { + if (entity instanceof DirtyCheckable && proxyHandler.isInitialized(entity)) { + PersistentEntity persistentEntity = mappingContext.getPersistentEntity(Hibernate.getClass(entity).getName()); + entity = proxyHandler.unwrap(entity); + DirtyCheckable dirtyCheckable = (DirtyCheckable) entity; + Map dirtyCheckingState = persistentEntity.getReflector().getDirtyCheckingState(entity); + if (dirtyCheckingState == null) { + dirtyCheckable.trackChanges(); + for (Embedded association : persistentEntity.getEmbedded()) { + if (DirtyCheckable.class.isAssignableFrom(association.getType())) { + Object embedded = association.getReader().read(entity); + if (embedded != null) { + DirtyCheckable embeddedCheck = (DirtyCheckable) embedded; + if (embeddedCheck.listDirtyPropertyNames().isEmpty()) { + embeddedCheck.trackChanges(); + } + } + } + } + } + } + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/DataSourceFactoryBean.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/DataSourceFactoryBean.groovy new file mode 100644 index 00000000000..7268f99205d --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/DataSourceFactoryBean.groovy @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.support + +import javax.sql.DataSource + +import org.springframework.beans.factory.FactoryBean + +import org.grails.orm.hibernate.AbstractHibernateDatastore +import org.grails.orm.hibernate.connections.HibernateConnectionSource + +/** + * A factory class to retrieve a {@link javax.sql.DataSource} from the Hibernate datastore + * + * @author James Kleeh + */ +class DataSourceFactoryBean implements FactoryBean { + + AbstractHibernateDatastore datastore + String connectionName + + DataSourceFactoryBean(AbstractHibernateDatastore datastore, String connectionName) { + this.datastore = datastore + this.connectionName = connectionName + } + + @Override + DataSource getObject() throws Exception { + ((HibernateConnectionSource) datastore.connectionSources.getConnectionSource(connectionName)).dataSource + } + + @Override + Class getObjectType() { + DataSource + } + + @Override + boolean isSingleton() { + true + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDatastoreConnectionSourcesRegistrar.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDatastoreConnectionSourcesRegistrar.groovy new file mode 100644 index 00000000000..231da37c875 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDatastoreConnectionSourcesRegistrar.groovy @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.support + +import javax.sql.DataSource + +import groovy.transform.CompileStatic + +import org.hibernate.SessionFactory + +import org.springframework.beans.BeansException +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.beans.factory.config.ConstructorArgumentValues +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.core.Ordered +import org.springframework.transaction.PlatformTransactionManager + +import org.grails.datastore.gorm.bootstrap.support.InstanceFactoryBean +import org.grails.datastore.mapping.config.Settings +import org.grails.datastore.mapping.core.connections.ConnectionSource +import org.grails.datastore.mapping.core.grailsversion.GrailsVersion + +/** + * A factory bean that looks up a datastore by connection name + * + * @author Graeme Rocher + * @since 6.0.6 + */ +@CompileStatic +class HibernateDatastoreConnectionSourcesRegistrar implements BeanDefinitionRegistryPostProcessor, Ordered { + + final Iterable dataSourceNames + + HibernateDatastoreConnectionSourcesRegistrar(Iterable dataSourceNames) { + this.dataSourceNames = dataSourceNames + } + + @Override + void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + for (String dataSourceName in dataSourceNames) { + boolean isDefault = dataSourceName == ConnectionSource.DEFAULT || dataSourceName == Settings.SETTING_DATASOURCE + boolean shouldConfigureDataSourceBean = GrailsVersion.isAtLeastMajorMinor(3, 3) + String dataSourceBeanName = isDefault ? Settings.SETTING_DATASOURCE : "${Settings.SETTING_DATASOURCE}_$dataSourceName" + + if (!registry.containsBeanDefinition(dataSourceBeanName) && shouldConfigureDataSourceBean) { + def dataSourceBean = new RootBeanDefinition() + dataSourceBean.setTargetType(DataSource) + dataSourceBean.setBeanClass(InstanceFactoryBean) + def args = new ConstructorArgumentValues() + String spel = "#{dataSourceConnectionSourceFactory.create('$dataSourceName', environment).source}".toString() + args.addGenericArgumentValue(spel) + dataSourceBean.setConstructorArgumentValues( + args + ) + registry.registerBeanDefinition(dataSourceBeanName, dataSourceBean) + } + + if (!isDefault) { + String suffix = '_' + dataSourceName + String sessionFactoryName = "sessionFactory$suffix" + String transactionManagerBeanName = "transactionManager$suffix" + + def sessionFactoryBean = new RootBeanDefinition() + sessionFactoryBean.setTargetType(SessionFactory) + sessionFactoryBean.setBeanClass(InstanceFactoryBean) + def args = new ConstructorArgumentValues() + args.addGenericArgumentValue("#{hibernateDatastore.getDatastoreForConnection('$dataSourceName').sessionFactory}".toString()) + sessionFactoryBean.setConstructorArgumentValues( + args + ) + registry.registerBeanDefinition( + sessionFactoryName, + sessionFactoryBean + ) + + def transactionManagerBean = new RootBeanDefinition() + transactionManagerBean.setTargetType(PlatformTransactionManager) + transactionManagerBean.setBeanClass(InstanceFactoryBean) + def txMgrArgs = new ConstructorArgumentValues() + txMgrArgs.addGenericArgumentValue("#{hibernateDatastore.getDatastoreForConnection('$dataSourceName').transactionManager}".toString()) + transactionManagerBean.setConstructorArgumentValues( + txMgrArgs + ) + registry.registerBeanDefinition( + transactionManagerBeanName, + transactionManagerBean + ) + } + } + } + + @Override + void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + // no-op + } + + @Override + int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 100 + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDatastoreFactoryBean.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDatastoreFactoryBean.groovy new file mode 100644 index 00000000000..f8724fd021c --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDatastoreFactoryBean.groovy @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.support + +import groovy.transform.CompileStatic + +import org.hibernate.SessionFactory + +import org.springframework.beans.BeansException +import org.springframework.beans.factory.FactoryBean +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware +import org.springframework.core.env.PropertyResolver + +import org.grails.datastore.mapping.model.MappingContext +import org.grails.orm.hibernate.AbstractHibernateDatastore + +/** + * Helper for constructing the datastore + * + * @author Graeme Rocher + * @since 5.0 + */ +@CompileStatic +class HibernateDatastoreFactoryBean implements FactoryBean, ApplicationContextAware { + + private final Class objectType + private final MappingContext mappingContext + private final SessionFactory sessionFactory + private final PropertyResolver configuration + private final String dataSourceName + ApplicationContext applicationContext + + HibernateDatastoreFactoryBean(Class objectType, MappingContext mappingContext, SessionFactory sessionFactory, PropertyResolver configuration, String dataSourceName) { + this.objectType = objectType + this.mappingContext = mappingContext + this.sessionFactory = sessionFactory + this.configuration = configuration + this.dataSourceName = dataSourceName + } + + @Override + void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext + } + + @Override + T getObject() throws Exception { + AbstractHibernateDatastore datastore = objectType.newInstance(mappingContext, sessionFactory, configuration, dataSourceName) + + if (applicationContext != null) { + datastore.setApplicationContext(applicationContext) + } + + return datastore + } + + @Override + Class getObjectType() { + return objectType + } + + @Override + boolean isSingleton() { + return true + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDialectDetectorFactoryBean.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDialectDetectorFactoryBean.java new file mode 100644 index 00000000000..191dde207c5 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateDialectDetectorFactoryBean.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.support; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.hibernate.HibernateException; +import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl; +import org.hibernate.boot.registry.selector.internal.StrategySelectorImpl; +import org.hibernate.boot.registry.selector.spi.StrategySelector; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl; +import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; +import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; +import org.hibernate.engine.jdbc.dialect.spi.DialectFactory; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfoSource; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolver; +import org.hibernate.service.Service; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.service.spi.ServiceBinding; +import org.hibernate.service.spi.ServiceRegistryImplementor; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import org.grails.orm.hibernate.exceptions.CouldNotDetermineHibernateDialectException; + +/** + * @author Steven Devijver + * @author Graeme Rocher + * @author Burt Beckwith + */ +public class HibernateDialectDetectorFactoryBean implements FactoryBean, InitializingBean { + + private DataSource dataSource; + private Properties vendorNameDialectMappings; + private String hibernateDialectClassName; + private Dialect hibernateDialect; + private Properties hibernateProperties = new Properties(); + + public void setHibernateProperties(Properties hibernateProperties) { + this.hibernateProperties = hibernateProperties; + } + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + public void setVendorNameDialectMappings(Properties mappings) { + vendorNameDialectMappings = mappings; + } + + public String getObject() { + return hibernateDialectClassName; + } + + public Class getObjectType() { + return String.class; + } + + public boolean isSingleton() { + return true; + } + + public void afterPropertiesSet() throws MetaDataAccessException { + Assert.notNull(dataSource, "Data source is not set!"); + Assert.notNull(vendorNameDialectMappings, "Vendor name/dialect mappings are not set!"); + + Connection connection = null; + + String dbName = (String) JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName"); + + try { + connection = DataSourceUtils.getConnection(dataSource); + + try { + final DialectFactory dialectFactory = createDialectFactory(); + final Connection finalConnection = connection; + DialectResolutionInfoSource infoSource = new DialectResolutionInfoSource() { + @Override + public DialectResolutionInfo getDialectResolutionInfo() { + try { + return new DatabaseMetaDataDialectResolutionInfoAdapter(finalConnection.getMetaData()); + } catch (SQLException e) { + throw new CouldNotDetermineHibernateDialectException( + "Could not determine Hibernate dialect", e); + } + } + }; + hibernateDialect = dialectFactory.buildDialect(hibernateProperties, infoSource); + hibernateDialectClassName = hibernateDialect.getClass().getName(); + } catch (HibernateException e) { + hibernateDialectClassName = vendorNameDialectMappings.getProperty(dbName); + } + + if (!StringUtils.hasText(hibernateDialectClassName)) { + throw new CouldNotDetermineHibernateDialectException( + "Could not determine Hibernate dialect for database name [" + dbName + "]!"); + } + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } + + // should be using the ServiceRegistry, but getting it from the SessionFactory at startup fails in Spring + protected DialectFactory createDialectFactory() { + DialectFactoryImpl factory = new DialectFactoryImpl(); + factory.injectServices(new ServiceRegistryImplementor() { + + @Override + public R getService(Class serviceRole) { + if (serviceRole == DialectResolver.class) { + return (R) new StandardDialectResolver(); + } else if (serviceRole == StrategySelector.class) { + return (R) new StrategySelectorImpl(new ClassLoaderServiceImpl(Thread.currentThread().getContextClassLoader())); + } + return null; + } + + @Override + public ServiceBinding locateServiceBinding(Class serviceRole) { + return null; + } + + @Override + public void destroy() { + + } + + @Override + public void registerChild(ServiceRegistryImplementor child) { + } + + @Override + public void deRegisterChild(ServiceRegistryImplementor child) { + } + + @Override + public ServiceRegistry getParentServiceRegistry() { + return null; + } + }); + return factory; + } + +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy new file mode 100644 index 00000000000..8f8290dad3d --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.support + +import groovy.transform.CompileStatic +import org.codehaus.groovy.runtime.StringGroovyMethods + +import org.hibernate.Session +import org.hibernate.SessionFactory + +import org.springframework.core.convert.ConversionService +import org.springframework.validation.Errors +import org.springframework.validation.FieldError + +import org.grails.datastore.gorm.GormValidateable +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.config.GormProperties +import org.grails.datastore.mapping.model.types.Association +import org.grails.datastore.mapping.model.types.OneToOne +import org.grails.datastore.mapping.proxy.ProxyHandler +import org.grails.datastore.mapping.validation.ValidationErrors +import org.grails.orm.hibernate.proxy.HibernateProxyHandler + +/** + * Utility methods used at runtime by the GORM for Hibernate implementation + * + * @author Graeme Rocher + * @since 4.0 + */ +@CompileStatic +class HibernateRuntimeUtils { + + private static ProxyHandler proxyHandler = new HibernateProxyHandler() + + private static final String DYNAMIC_FILTER_ENABLER = 'dynamicFilterEnabler' + + @SuppressWarnings('rawtypes') + static void enableDynamicFilterEnablerIfPresent(SessionFactory sessionFactory, Session session) { + if (sessionFactory != null && session != null) { + final Set definedFilterNames = sessionFactory.getDefinedFilterNames() + if (definedFilterNames != null && definedFilterNames.contains(DYNAMIC_FILTER_ENABLER)) + session.enableFilter(DYNAMIC_FILTER_ENABLER) // work around for HHH-2624 + } + } + + /** + * Initializes the Errors property on target. The target will be assigned a new + * Errors property. If the target contains any binding errors, those binding + * errors will be copied in to the new Errors property. + * + * @param target object to initialize + * @return the new Errors object + */ + static Errors setupErrorsProperty(Object target) { + + boolean isGormValidateable = target instanceof GormValidateable + + MetaClass mc = isGormValidateable ? null : GroovySystem.metaClassRegistry.getMetaClass(target.getClass()) + def errors = new ValidationErrors(target) + + Errors originalErrors = isGormValidateable ? ((GormValidateable) target).getErrors() : (Errors) mc.getProperty(target, GormProperties.ERRORS) + for (Object o in originalErrors.fieldErrors) { + FieldError fe = (FieldError) o + if (fe.isBindingFailure()) { + errors.addError(new FieldError(fe.getObjectName(), + fe.field, + fe.rejectedValue, + fe.bindingFailure, + fe.codes, + fe.arguments, + fe.defaultMessage)) + } + } + + if (isGormValidateable) { + ((GormValidateable) target).setErrors(errors) + } + else { + mc.setProperty(target, GormProperties.ERRORS, errors) + } + return errors + } + + static void autoAssociateBidirectionalOneToOnes(PersistentEntity entity, Object target) { + def mappingContext = entity.mappingContext + for (Association association : entity.associations) { + if (!(association instanceof OneToOne) || !association.bidirectional || !association.owningSide) { + continue + } + + def propertyName = association.name + if (!proxyHandler.isInitialized(target, propertyName)) { + continue + } + + def otherSide = association.inverseSide + + if (otherSide == null) { + continue + } + + def entityReflector = mappingContext.getEntityReflector(entity) + Object inverseObject = entityReflector.getProperty(target, propertyName) + if (inverseObject == null) { + continue + } + + def otherSidePropertyName = otherSide.getName() + if (!proxyHandler.isInitialized(inverseObject, otherSidePropertyName)) { + continue + } + + def associationReflector = mappingContext.getEntityReflector(association.associatedEntity) + def propertyValue = associationReflector.getProperty(inverseObject, otherSidePropertyName) + if (propertyValue == null) { + associationReflector.setProperty(inverseObject, otherSidePropertyName, target) + } + } + } + + static Object convertValueToType(Object passedValue, Class targetType, ConversionService conversionService) { + // workaround for GROOVY-6127, do not assign directly in parameters before it's fixed + Object value = passedValue + if (targetType != null && value != null && !(value in targetType)) { + if (value instanceof CharSequence) { + value = value.toString() + if (value in targetType) { + return value + } + } + try { + if (value instanceof Number && (targetType == Long || targetType == Integer)) { + if (targetType == Long) { + value = ((Number) value).toLong() + } else { + value = ((Number) value).toInteger() + } + } else if (value instanceof String && targetType in Number) { + String strValue = value.trim() + if (targetType == Long) { + value = Long.parseLong(strValue) + } else if (targetType == Integer) { + value = Integer.parseInt(strValue) + } else { + value = StringGroovyMethods.asType(strValue, targetType) + } + } else { + value = conversionService.convert(value, targetType) + } + } catch (e) { + // ignore + } + } + return value + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateVersionSupport.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateVersionSupport.java new file mode 100644 index 00000000000..71970b80c28 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateVersionSupport.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.support; + +import org.hibernate.FlushMode; +import org.hibernate.Hibernate; +import org.hibernate.Query; +import org.hibernate.Session; + +import org.grails.datastore.mapping.core.grailsversion.GrailsVersion; + +/** + * + * Methods to deal with the differences in different Hibernate versions + * + * @author Graeme Rocher + * @author Juergen Hoeller + * + * @since 6.0 + * + */ +public class HibernateVersionSupport { + + /** + * Get the native Hibernate FlushMode, adapting between Hibernate 5.0/5.1 and 5.2+. + * @param session the Hibernate Session to get the flush mode from + * @return the FlushMode (never {@code null}) + * @since 4.3 + * @deprecated Previously used for Hibernate backwards, will be removed in a future release. + */ + @Deprecated + public static FlushMode getFlushMode(Session session) { + return session.getHibernateFlushMode(); + } + + /** + * Set the native Hibernate FlushMode, adapting between Hibernate 5.0/5.1 and 5.2+. + * @param session the Hibernate Session to get the flush mode from + * @since 4.3 + * @deprecated Previously used for Hibernate backwards, will be removed in a future release. + */ + @Deprecated + public static void setFlushMode(Session session, FlushMode flushMode) { + session.setHibernateFlushMode(flushMode); + } + + /** + * Check the current hibernate version + * @param required The required version + * @return True if it is at least the given version + */ + public static boolean isAtLeastVersion(String required) { + String hibernateVersion = Hibernate.class.getPackage().getImplementationVersion(); + if (hibernateVersion != null) { + return GrailsVersion.isAtLeast(hibernateVersion, required); + } else { + return false; + } + } + + /** + * Creates a query + * + * @param session The session + * @param query The query + * @return The created query + * @deprecated Previously used for Hibernate backwards, will be removed in a future release. + */ + @Deprecated + public static Query createQuery(Session session, String query) { + return session.createQuery(query); + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/SoftKey.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/SoftKey.java new file mode 100644 index 00000000000..245c505e811 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/SoftKey.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.support; + +import java.lang.ref.SoftReference; + +/** + * SoftReference key to be used with ConcurrentHashMap. + * + * @author Lari Hotari + */ +public class SoftKey extends SoftReference { + final int hash; + + public SoftKey(T referent) { + super(referent); + hash = referent.hashCode(); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + @SuppressWarnings("unchecked") + SoftKey other = (SoftKey) obj; + if (hash != other.hash) { + return false; + } + T referent = get(); + T otherReferent = other.get(); + if (referent == null) { + if (otherReferent != null) { + return false; + } + } + else if (!referent.equals(otherReferent)) { + return false; + } + return true; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/transaction/HibernateJtaTransactionManagerAdapter.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/transaction/HibernateJtaTransactionManagerAdapter.java new file mode 100644 index 00000000000..c861eaa5f75 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/transaction/HibernateJtaTransactionManagerAdapter.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.transaction; + +import javax.transaction.xa.XAResource; + +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.Synchronization; +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionManager; + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Adapter for adding transaction controlling hooks for supporting + * Hibernate's org.hibernate.engine.transaction.Isolater class's interaction with transactions + * + * This is required when there is no real JTA transaction manager in use and Spring's + * {@link org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy} is used. + * + * Without this solution, using Hibernate's TableGenerator identity strategies will fail to support transactions. + * The id generator will commit the current transaction and break transactional behaviour. + * + * The javadoc of Hibernate's {@code TableHiLoGenerator} states this. However this isn't mentioned in the javadocs of other TableGenerators. + * + * @author Lari Hotari + */ +public class HibernateJtaTransactionManagerAdapter implements TransactionManager { + PlatformTransactionManager springTransactionManager; + ThreadLocal currentTransactionHolder = new ThreadLocal<>(); + + public HibernateJtaTransactionManagerAdapter(PlatformTransactionManager springTransactionManager) { + this.springTransactionManager = springTransactionManager; + } + + @Override + public void begin() { + TransactionDefinition definition = new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + currentTransactionHolder.set(springTransactionManager.getTransaction(definition)); + } + + @Override + public void commit() throws + SecurityException, IllegalStateException { + springTransactionManager.commit(getAndRemoveStatus()); + } + + @Override + public void rollback() throws IllegalStateException, SecurityException { + springTransactionManager.rollback(getAndRemoveStatus()); + } + + @Override + public void setRollbackOnly() throws IllegalStateException { + currentTransactionHolder.get().setRollbackOnly(); + } + + protected TransactionStatus getAndRemoveStatus() { + TransactionStatus status = currentTransactionHolder.get(); + currentTransactionHolder.remove(); + return status; + } + + @Override + public int getStatus() { + TransactionStatus status = currentTransactionHolder.get(); + return convertToJtaStatus(status); + } + + protected static int convertToJtaStatus(TransactionStatus status) { + if (status != null) { + if (status.isCompleted()) { + return Status.STATUS_UNKNOWN; + } else if (status.isRollbackOnly()) { + return Status.STATUS_MARKED_ROLLBACK; + } else { + return Status.STATUS_ACTIVE; + } + } else { + return Status.STATUS_NO_TRANSACTION; + } + } + + @Override + public Transaction getTransaction() { + return new TransactionAdapter(springTransactionManager, currentTransactionHolder); + } + + @Override + public void resume(Transaction tobj) throws IllegalStateException { + TransactionAdapter transaction = (TransactionAdapter) tobj; + // commit the PROPAGATION_NOT_SUPPORTED transaction returned in suspend + springTransactionManager.commit(transaction.transactionStatus); + } + + @Override + public Transaction suspend() { + currentTransactionHolder.set(springTransactionManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_NOT_SUPPORTED))); + return new TransactionAdapter(springTransactionManager, currentTransactionHolder); + } + + @Override + public void setTransactionTimeout(int seconds) { + + } + + private static class TransactionAdapter implements Transaction { + PlatformTransactionManager springTransactionManager; + TransactionStatus transactionStatus; + ThreadLocal currentTransactionHolder; + + TransactionAdapter(PlatformTransactionManager springTransactionManager, ThreadLocal currentTransactionHolder) { + this.springTransactionManager = springTransactionManager; + this.currentTransactionHolder = currentTransactionHolder; + this.transactionStatus = currentTransactionHolder.get(); + } + + @Override + public void commit() throws + SecurityException, IllegalStateException { + springTransactionManager.commit(transactionStatus); + currentTransactionHolder.remove(); + } + + @Override + public boolean delistResource(XAResource xaRes, int flag) throws IllegalStateException, SystemException { + return false; + } + + @Override + public boolean enlistResource(XAResource xaRes) throws RollbackException, IllegalStateException, + SystemException { + return false; + } + + @Override + public int getStatus() throws SystemException { + return convertToJtaStatus(transactionStatus); + } + + @Override + public void registerSynchronization(final Synchronization sync) throws RollbackException, IllegalStateException, + SystemException { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void beforeCompletion() { + sync.beforeCompletion(); + } + + @Override + public void afterCompletion(int status) { + int jtaStatus; + if (status == TransactionSynchronization.STATUS_COMMITTED) { + jtaStatus = Status.STATUS_COMMITTED; + } else if (status == TransactionSynchronization.STATUS_ROLLED_BACK) { + jtaStatus = Status.STATUS_ROLLEDBACK; + } else { + jtaStatus = Status.STATUS_UNKNOWN; + } + sync.afterCompletion(jtaStatus); + } + + public void suspend() { } + + public void resume() { } + + public void flush() { } + + public void beforeCommit(boolean readOnly) { } + + public void afterCommit() { } + }); + } + + @Override + public void rollback() throws IllegalStateException, SystemException { + springTransactionManager.rollback(transactionStatus); + currentTransactionHolder.remove(); + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + transactionStatus.setRollbackOnly(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj == null) { + return false; + } else if (obj.getClass() == TransactionAdapter.class) { + TransactionAdapter other = (TransactionAdapter) obj; + if (other.transactionStatus == this.transactionStatus) { + return true; + } else if (other.transactionStatus != null) { + return other.transactionStatus.equals(this.transactionStatus); + } else { + return false; + } + } else { + return false; + } + } + + @Override + public int hashCode() { + return transactionStatus != null ? transactionStatus.hashCode() : System.identityHashCode(this); + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/transaction/PlatformTransactionManagerProxy.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/transaction/PlatformTransactionManagerProxy.java new file mode 100644 index 00000000000..ae7b8ccee57 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/transaction/PlatformTransactionManagerProxy.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.orm.hibernate.transaction; + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; + +/** + * A proxy for the {@link org.springframework.transaction.PlatformTransactionManager} instance + * + * @author Graeme Rocher + * @author Burt Beckwith + */ +public class PlatformTransactionManagerProxy implements PlatformTransactionManager { + private PlatformTransactionManager targetTransactionManager; + + public PlatformTransactionManagerProxy() { + + } + + public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + return targetTransactionManager.getTransaction(definition); + } + + public void commit(TransactionStatus status) throws TransactionException { + targetTransactionManager.commit(status); + } + + public void rollback(TransactionStatus status) throws TransactionException { + targetTransactionManager.rollback(status); + } + + public PlatformTransactionManager getTargetTransactionManager() { + return targetTransactionManager; + } + + public void setTargetTransactionManager(PlatformTransactionManager targetTransactionManager) { + this.targetTransactionManager = targetTransactionManager; + } +} diff --git a/grails-data-hibernate7/core/src/main/resources/META-INF/org.hibernate.integrator.spi.Integrator b/grails-data-hibernate7/core/src/main/resources/META-INF/org.hibernate.integrator.spi.Integrator new file mode 100644 index 00000000000..986dd2c1ed1 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/resources/META-INF/org.hibernate.integrator.spi.Integrator @@ -0,0 +1 @@ +org.grails.orm.hibernate.EventListenerIntegrator \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-data-hibernate7/core/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..7a512604174 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,28 @@ +{ + "groups": [ + { + "name": "hibernate.cache", + "description": "Hibernate" + } + ], + "properties": [ + { + "name": "hibernate.cache.queries", + "type": "java.lang.Boolean", + "description": "Whether to cache Hibernate queries.", + "defaultValue": false + }, + { + "name": "hibernate.cache.use_second_level_cache", + "type": "java.lang.Boolean", + "description": "Whether to enable Hibernate's second-level cache.", + "defaultValue": false + }, + { + "name": "hibernate.cache.use_query_cache", + "type": "java.lang.Boolean", + "description": "Whether to enable Hibernate's query cache.", + "defaultValue": false + } + ] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy new file mode 100644 index 00000000000..ec22c953c9e --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy @@ -0,0 +1,902 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.hibernate.mapping + +import org.grails.orm.hibernate.cfg.CompositeIdentity +import org.grails.orm.hibernate.cfg.HibernateMappingBuilder + +/** + * Created by graemerocher on 01/02/2017. + */ + +import org.grails.orm.hibernate.cfg.PropertyConfig +import org.hibernate.FetchMode +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertNull +import static org.junit.jupiter.api.Assertions.assertThrows +import static org.junit.jupiter.api.Assertions.assertTrue + +/** + * Tests that the Hibernate mapping DSL constructs a valid Mapping object. + * + * @author Graeme Rocher + * @since 1.0 + */ +class HibernateMappingBuilderTests { + +// void testWildcardApplyToAllProperties() { +// def builder = new HibernateMappingBuilder("Foo") +// def mapping = builder.evaluate { +// '*'(column:"foo") +// '*-1'(column:"foo") +// '1-1'(column:"foo") +// '1-*'(column:"foo") +// '*-*'(column:"foo") +// one cache:true +// two ignoreNoteFound:false +// } +// } + + @Test + void testIncludes() { + def callable = { + foos lazy:false + } + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + includes callable + foos ignoreNotFound:true + } + + def pc = mapping.getPropertyConfig("foos") + assert pc.ignoreNotFound : "should have ignoreNotFound enabled" + assert !pc.lazy : "should not be lazy" + } + + @Test + void testIgnoreNotFound() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + foos ignoreNotFound:true + } + + assertTrue mapping.getPropertyConfig("foos").ignoreNotFound, "ignore not found should have been true" + + mapping = builder.evaluate { + foos ignoreNotFound:false + } + assertFalse mapping.getPropertyConfig("foos").ignoreNotFound, "ignore not found should have been false" + + mapping = builder.evaluate { // default + foos lazy:false + } + assertFalse mapping.getPropertyConfig("foos").ignoreNotFound, "ignore not found should have been false" + } + + @Test + void testNaturalId() { + + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + id natural: 'one' + } + + assertEquals(['one'], mapping.identity.natural.propertyNames) + + mapping = builder.evaluate { + id natural: ['one','two'] + } + + assertEquals(['one','two'], mapping.identity.natural.propertyNames) + + mapping = builder.evaluate { + id natural: [properties:['one','two'], mutable:true] + } + + assertEquals(['one','two'], mapping.identity.natural.propertyNames) + assertTrue mapping.identity.natural.mutable + } + + @Test + void testDiscriminator() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + discriminator 'one' + } + + assertEquals "one", mapping.discriminator.value + assertNull mapping.discriminator.column + + mapping = builder.evaluate { + discriminator value:'one', column:'type' + } + + assertEquals "one", mapping.discriminator.value + assertEquals "type", mapping.discriminator.column.name + + mapping = builder.evaluate { + discriminator value:'one', column:[name:'type', sqlType:'integer'] + } + + assertEquals "one", mapping.discriminator.value + assertEquals "type", mapping.discriminator.column.name + assertEquals "integer", mapping.discriminator.column.sqlType + } + + @Test + void testDiscriminatorMap() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + discriminator value:'1', formula:"case when CLASS_TYPE in ('a', 'b', 'c') then 0 else 1 end",type:'integer',insert:false + } + + assertEquals "1", mapping.discriminator.value + assertNull mapping.discriminator.column + + assertEquals "case when CLASS_TYPE in ('a', 'b', 'c') then 0 else 1 end", mapping.discriminator.formula + assertEquals "integer", mapping.discriminator.type + assertFalse mapping.discriminator.insertable + } + + @Test + void testAutoImport() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { } + + assertTrue mapping.autoImport, "default auto-import should be true" + + mapping = builder.evaluate { + autoImport false + } + + assertFalse mapping.autoImport, "auto-import should be false" + } + + @Test + void testTableWithCatalogueAndSchema() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table name:"table", catalog:"CRM", schema:"dbo" + } + + assertEquals 'table',mapping.table.name + assertEquals 'dbo',mapping.table.schema + assertEquals 'CRM',mapping.table.catalog + } + + @Test + void testIndexColumn() { + + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + things indexColumn:[name:"chapter_number", type:"string", length:3] + } + + PropertyConfig pc = mapping.getPropertyConfig("things") + assertEquals "chapter_number",pc.indexColumn.column + assertEquals "string",pc.indexColumn.type + assertEquals 3, pc.indexColumn.length + } + + @Test + void testDynamicUpdate() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + dynamicUpdate true + dynamicInsert true + } + + assertTrue mapping.dynamicUpdate + assertTrue mapping.dynamicInsert + + builder = new HibernateMappingBuilder("Foo") + mapping = builder.evaluate {} + + assertFalse mapping.dynamicUpdate + assertFalse mapping.dynamicInsert + } + + @Test + void testBatchSizeConfig() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + batchSize 10 + things batchSize:15 + } + + assertEquals 10, mapping.batchSize + assertEquals 15,mapping.getPropertyConfig('things').batchSize + } + + @Test + void testChangeVersionColumn() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + version 'v_number' + } + + assertEquals 'v_number', mapping.getPropertyConfig("version").column + } + + @Test + void testClassSortOrder() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + sort "name" + order "desc" + columns { + things sort:'name' + } + } + + assertEquals "name", mapping.sort.name + assertEquals "desc", mapping.sort.direction + assertEquals 'name',mapping.getPropertyConfig('things').sort + + mapping = builder.evaluate { + sort name:'desc' + + columns { + things sort:'name' + } + } + + assertEquals "name", mapping.sort.name + assertEquals "desc", mapping.sort.direction + assertEquals 'name',mapping.getPropertyConfig('things').sort + } + + @Test + void testAssociationSortOrder() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + columns { + things sort:'name' + } + } + + assertEquals 'name',mapping.getPropertyConfig('things').sort + } + + @Test + void testLazy() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + columns { + things cascade:'save-update' + } + } + + assertNull mapping.getPropertyConfig('things').getLazy(), "should have been lazy" + + mapping = builder.evaluate { + columns { + things lazy:false + } + } + + assertFalse mapping.getPropertyConfig('things').lazy, "shouldn't have been lazy" + } + + @Test + void testCascades() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + columns { + things cascade:'save-update' + } + } + + assertEquals 'save-update',mapping.getPropertyConfig('things').cascade + } + + @Test + void testFetchModes() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + columns { + things fetch:'join' + others fetch:'select' + mores column:'yuck' + } + } + + assertEquals FetchMode.JOIN,mapping.getPropertyConfig('things').fetchMode + assertEquals FetchMode.SELECT,mapping.getPropertyConfig('others').fetchMode + assertEquals FetchMode.DEFAULT,mapping.getPropertyConfig('mores').fetchMode + } + + @Test + void testEnumType() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + columns { + things column:'foo' + } + } + + assertEquals 'default',mapping.getPropertyConfig('things').enumType + + mapping = builder.evaluate { + columns { + things enumType:'ordinal' + } + } + + assertEquals 'ordinal',mapping.getPropertyConfig('things').enumType + } + + @Test + void testCascadesWithColumnsBlock() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + things cascade:'save-update' + } + assertEquals 'save-update',mapping.getPropertyConfig('things').cascade + } + + @Test + void testJoinTableMapping() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + columns { + things joinTable:true + } + } + + assert mapping.getPropertyConfig('things')?.joinTable + + mapping = builder.evaluate { + columns { + things joinTable:'foo' + } + } + + PropertyConfig property = mapping.getPropertyConfig('things') + assert property?.joinTable + assertEquals "foo", property.joinTable.name + + mapping = builder.evaluate { + columns { + things joinTable:[name:'foo', key:'foo_id', column:'bar_id'] + } + } + + property = mapping.getPropertyConfig('things') + assert property?.joinTable + assertEquals "foo", property.joinTable.name + assertEquals "foo_id", property.joinTable.key.name + assertEquals "bar_id", property.joinTable.column.name + } + + @Test + void testJoinTableMappingWithoutColumnsBlock() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + things joinTable:true + } + + assert mapping.getPropertyConfig('things')?.joinTable + + mapping = builder.evaluate { + things joinTable:'foo' + } + + PropertyConfig property = mapping.getPropertyConfig('things') + assert property?.joinTable + assertEquals "foo", property.joinTable.name + + mapping = builder.evaluate { + things joinTable:[name:'foo', key:'foo_id', column:'bar_id'] + } + + property = mapping.getPropertyConfig('things') + assert property?.joinTable + assertEquals "foo", property.joinTable.name + assertEquals "foo_id", property.joinTable.key.name + assertEquals "bar_id", property.joinTable.column.name + } + + @Test + void testCustomInheritanceStrategy() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + tablePerHierarchy false + } + + assertFalse mapping.tablePerHierarchy + + mapping = builder.evaluate { + table 'myTable' + tablePerSubclass true + } + + assertFalse mapping.tablePerHierarchy + } + + @Test + void testAutoTimeStamp() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + autoTimestamp false + } + + assertFalse mapping.autoTimestamp + } + + @Test + void testCustomAssociationCachingConfig1() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + columns { + firstName cache:[usage:'read-only', include:'non-lazy'] + } + } + + def cc = mapping.getPropertyConfig('firstName') + assertEquals 'read-only', cc.cache.usage + assertEquals 'non-lazy', cc.cache.include + } + + @Test + void testCustomAssociationCachingConfig1WithoutColumnsBlock() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + firstName cache:[usage:'read-only', include:'non-lazy'] + } + + def cc = mapping.getPropertyConfig('firstName') + assertEquals 'read-only', cc.cache.usage + assertEquals 'non-lazy', cc.cache.include + } + + @Test + void testCustomAssociationCachingConfig2() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + + columns { + firstName cache:'read-only' + } + } + + def cc = mapping.getPropertyConfig('firstName') + assertEquals 'read-only', cc.cache.usage + } + + @Test + void testCustomAssociationCachingConfig2WithoutColumnsBlock() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + firstName cache:'read-only' + } + + def cc = mapping.getPropertyConfig('firstName') + assertEquals 'read-only', cc.cache.usage + } + + @Test + void testAssociationCachingConfig() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + + columns { + firstName cache:true + } + } + + def cc = mapping.getPropertyConfig('firstName') + assertEquals 'read-write', cc.cache.usage + assertEquals 'all', cc.cache.include + } + + @Test + void testAssociationCachingConfigWithoutColumnsBlock() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + firstName cache:true + } + + def cc = mapping.getPropertyConfig('firstName') + assertEquals 'read-write', cc.cache.usage + assertEquals 'all', cc.cache.include + } + + @Test + void testEvaluateTableName() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + } + + assertEquals 'myTable', mapping.tableName + } + + @Test + void testDefaultCacheStrategy() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + cache true + } + + assertEquals 'read-write', mapping.cache.usage + assertEquals 'all', mapping.cache.include + } + + @Test + void testCustomCacheStrategy() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + cache usage:'read-only', include:'non-lazy' + } + + assertEquals 'read-only', mapping.cache.usage + assertEquals 'non-lazy', mapping.cache.include + } + + @Test + void testCustomCacheStrategy2() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + cache 'read-only' + } + + assertEquals 'read-only', mapping.cache.usage + assertEquals 'all', mapping.cache.include + } + + @Test + void testInvalidCacheValues() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + cache usage:'rubbish', include:'more-rubbish' + } + + // should be ignored and logged to console + assertEquals 'read-write', mapping.cache.usage + assertEquals 'all', mapping.cache.include + } + + @Test + void testEvaluateVersioning() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + version false + } + + assertEquals 'myTable', mapping.tableName + assertFalse mapping.versioned + } + + @Test + void testIdentityColumnMapping() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + version false + id column:'foo_id', type:Integer + } + + assertEquals Long, mapping.identity.type + assertEquals 'foo_id', mapping.getPropertyConfig("id").column + assertEquals Integer, mapping.getPropertyConfig("id").type + assertEquals 'native', mapping.identity.generator + } + + @Test + void testDefaultIdStrategy() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + version false + } + + assertEquals Long, mapping.identity.type + assertEquals 'id', mapping.identity.column + assertEquals 'native', mapping.identity.generator + } + + @Test + void testHiloIdStrategy() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + version false + id generator:'hilo', params:[table:'hi_value',column:'next_value',max_lo:100] + } + + assertEquals Long, mapping.identity.type + assertEquals 'id', mapping.identity.column + assertEquals 'hilo', mapping.identity.generator + assertEquals 'hi_value', mapping.identity.params.table + } + + @Test + void testCompositeIdStrategy() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + version false + id composite:['one','two'], compositeClass:HibernateMappingBuilder + } + + assert mapping.identity instanceof CompositeIdentity + assertEquals "one", mapping.identity.propertyNames[0] + assertEquals "two", mapping.identity.propertyNames[1] + assertEquals HibernateMappingBuilder, mapping.identity.compositeClass + } + + @Test + void testSimpleColumnMappingsWithoutColumnsBlock() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + version false + firstName column:'First_Name' + lastName column:'Last_Name' + } + + assertEquals "First_Name",mapping.getPropertyConfig('firstName').column + assertEquals "Last_Name",mapping.getPropertyConfig('lastName').column + } + + @Test + void testSimpleColumnMappings() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + version false + columns { + firstName column:'First_Name' + lastName column:'Last_Name' + } + } + + assertEquals "First_Name",mapping.getPropertyConfig('firstName').column + assertEquals "Last_Name",mapping.getPropertyConfig('lastName').column + } + + @Test + void testComplexColumnMappings() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + version false + columns { + firstName column:'First_Name', + lazy:true, + unique:true, + type: java.sql.Clob, + length:255, + index:'foo', + sqlType: 'text' + + lastName column:'Last_Name' + } + } + + assertEquals "First_Name",mapping.columns.firstName.column + assertTrue mapping.columns.firstName.lazy + assertTrue mapping.columns.firstName.unique + assertEquals java.sql.Clob,mapping.columns.firstName.type + assertEquals 255,mapping.columns.firstName.length + assertEquals 'foo',mapping.columns.firstName.getIndexName() + assertEquals "text",mapping.columns.firstName.sqlType + assertEquals "Last_Name",mapping.columns.lastName.column + } + + @Test + void testComplexColumnMappingsWithoutColumnsBlock() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + table 'myTable' + version false + firstName column:'First_Name', + lazy:true, + unique:true, + type: java.sql.Clob, + length:255, + index:'foo', + sqlType: 'text' + + lastName column:'Last_Name' + } + + assertEquals "First_Name",mapping.columns.firstName.column + assertTrue mapping.columns.firstName.lazy + assertTrue mapping.columns.firstName.unique + assertEquals java.sql.Clob,mapping.columns.firstName.type + assertEquals 255,mapping.columns.firstName.length + assertEquals 'foo',mapping.columns.firstName.getIndexName() + assertEquals "text",mapping.columns.firstName.sqlType + assertEquals "Last_Name",mapping.columns.lastName.column + } + + @Test + void testPropertyWithMultipleColumns() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + amount type: MyUserType, { + column name: "value" + column name: "currency", sqlType: "char", length: 3 + } + } + + assertEquals 2, mapping.columns.amount.columns.size() + assertEquals "value", mapping.columns.amount.columns[0].name + assertEquals "currency", mapping.columns.amount.columns[1].name + assertEquals "char", mapping.columns.amount.columns[1].sqlType + assertEquals 3, mapping.columns.amount.columns[1].length + + assertThrows Throwable, { mapping.columns.amount.column } + assertThrows Throwable, { mapping.columns.amount.sqlType } + } + + @Test + void testConstrainedPropertyWithMultipleColumns() { + def builder = new HibernateMappingBuilder("Foo") + builder.evaluate { + amount type: MyUserType, { + column name: "value" + column name: "currency", sqlType: "char", length: 3 + } + } + def mapping = builder.evaluate { + amount nullable: true + } + + assertEquals 2, mapping.columns.amount.columns.size() + assertEquals "value", mapping.columns.amount.columns[0].name + assertEquals "currency", mapping.columns.amount.columns[1].name + assertEquals "char", mapping.columns.amount.columns[1].sqlType + assertEquals 3, mapping.columns.amount.columns[1].length + + assertThrows Throwable, { mapping.columns.amount.column } + assertThrows Throwable, { mapping.columns.amount.sqlType } + } + + @Test + void testDisallowedConstrainedPropertyWithMultipleColumns() { + def builder = new HibernateMappingBuilder("Foo") + builder.evaluate { + amount type: MyUserType, { + column name: "value" + column name: "currency", sqlType: "char", length: 3 + } + } + assertThrows(Throwable, { + builder.evaluate { + amount scale: 2 + } + }, "Cannot treat multi-column property as a single-column property") + } + + @Test + void testPropertyWithUserTypeAndNoParams() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + amount type: MyUserType + } + + assertEquals MyUserType, mapping.getPropertyConfig('amount').type + assertNull mapping.getPropertyConfig('amount').typeParams + } + + @Test + void testPropertyWithUserTypeAndTypeParams() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + amount type: MyUserType, params : [ param1 : "amountParam1", param2 : 65 ] + value type: MyUserType, params : [ param1 : "valueParam1", param2 : 21 ] + } + + assertEquals MyUserType, mapping.getPropertyConfig('amount').type + assertEquals "amountParam1", mapping.getPropertyConfig('amount').typeParams.param1 + assertEquals 65, mapping.getPropertyConfig('amount').typeParams.param2 + assertEquals MyUserType, mapping.getPropertyConfig('value').type + assertEquals "valueParam1", mapping.getPropertyConfig('value').typeParams.param1 + assertEquals 21, mapping.getPropertyConfig('value').typeParams.param2 + } + + @Test + void testInsertablePropertyConfig() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + firstName insertable:true + lastName insertable:false + } + assertTrue mapping.getPropertyConfig('firstName').insertable + assertFalse mapping.getPropertyConfig('lastName').insertable + } + + @Test + void testUpdateablePropertyConfig() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + firstName updateable:true + lastName updateable:false + } + assertTrue mapping.getPropertyConfig('firstName').updateable + assertFalse mapping.getPropertyConfig('lastName').updateable + } + + @Test + void testUpdatablePropertyConfig() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + firstName updatable:true + lastName updatable:false + } + assertTrue mapping.getPropertyConfig('firstName').updatable + assertFalse mapping.getPropertyConfig('lastName').updatable + } + + @Test + void testDefaultValue() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + comment 'wahoo' + name comment: 'bar' + foo defaultValue: '5' + } + assertEquals '5', mapping.getPropertyConfig('foo').columns[0].defaultValue + assertNull mapping.getPropertyConfig('name').columns[0].defaultValue + } + + @Test + void testColumnComment() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + comment 'wahoo' + name comment: 'bar' + foo defaultValue: '5' + } + assertEquals 'bar', mapping.getPropertyConfig('name').columns[0].comment + assertNull mapping.getPropertyConfig('foo').columns[0].comment + } + + @Test + void testTableComment() { + def builder = new HibernateMappingBuilder("Foo") + def mapping = builder.evaluate { + comment 'wahoo' + name comment: 'bar' + foo defaultValue: '5' + } + assertEquals 'wahoo', mapping.comment + } + // dummy user type + static class MyUserType {} +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateOptimisticLockingStyleMappingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateOptimisticLockingStyleMappingSpec.groovy new file mode 100644 index 00000000000..4d8f3e5f795 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateOptimisticLockingStyleMappingSpec.groovy @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.hibernate.mapping + +import grails.persistence.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.hibernate.boot.Metadata +import org.hibernate.engine.OptimisticLockStyle +import org.hibernate.mapping.PersistentClass + +class HibernateOptimisticLockingStyleMappingSpec extends GrailsDataTckSpec { + + void setupSpec() { + manager.domainClasses.addAll([HibernateOptLockingStyleVersioned, HibernateOptLockingStyleNotVersioned]) + } + + void testEvaluateHibernateOptimisticLockStyleIsDefined() { + setup: + Metadata hibernateMetadata = manager.hibernateDatastore.getMetadata() + + when: 'Find out Hibernate PersistentClass representations for our domains' + PersistentClass forVersioned = hibernateMetadata.getEntityBinding(HibernateOptLockingStyleVersioned.name) + PersistentClass forNotVersioned = hibernateMetadata.getEntityBinding(HibernateOptLockingStyleNotVersioned.name) + + then: + forVersioned.optimisticLockStyle == OptimisticLockStyle.VERSION + forNotVersioned.optimisticLockStyle == OptimisticLockStyle.NONE + } +} + + +@Entity +class HibernateOptLockingStyleVersioned implements Serializable { + Long id + Long version + + String name +} + +@Entity +class HibernateOptLockingStyleNotVersioned implements Serializable { + Long id + Long version + + String name + + static mapping = { + version false + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/MappingBuilderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/MappingBuilderSpec.groovy new file mode 100644 index 00000000000..876bbcd3c8f --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/MappingBuilderSpec.groovy @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.hibernate.mapping + +import org.grails.datastore.mapping.model.config.GormProperties +import org.grails.orm.hibernate.cfg.CompositeIdentity +import org.grails.orm.hibernate.cfg.Mapping +import org.grails.orm.hibernate.cfg.PropertyConfig +import spock.lang.Specification + +import jakarta.persistence.FetchType + +import static grails.gorm.hibernate.mapping.MappingBuilder.define +/** + * Created by graemerocher on 01/02/2017. + */ +class MappingBuilderSpec extends Specification { + + void "test basic table mapping configuration"() { + when: + Mapping mapping = define { + autowire false + table "test" + }.build() + + then: + !mapping.autowire + mapping.table.name == 'test' + } + + void "test complex table mapping"() { + given: + Mapping mapping = define { + table { + catalog "foo" + schema "bar" + name "test" + } + }.build() + + expect: + mapping.table.name == 'test' + mapping.table.catalog == 'foo' + mapping.table.schema == 'bar' + } + + void "test id mapping"() { + given: + Mapping mapping = define { + id { + name 'test' + generator 'native' + params foo:'bar' + } + }.build() + + expect: + mapping.identity.name == 'test' + mapping.identity.generator == 'native' + mapping.identity.params == [foo:'bar'] + } + + void "test composite id mapping"() { + given: + Mapping mapping = define { + id composite("foo", "bar").compositeClass(MappingBuilderSpec) + }.build() + + expect: + mapping.identity instanceof CompositeIdentity + mapping.identity.propertyNames == ['foo', 'bar'] + mapping.identity.compositeClass == MappingBuilderSpec + } + + void "test cache mapping"() { + given: + Mapping mapping = define { + cache { + enabled true + usage 'read' + include 'some' + } + }.build() + + expect: + mapping.cache.enabled + mapping.cache.usage == 'read' + mapping.cache.include == 'some' + } + + void "test sort mapping"() { + when: + Mapping mapping = define { + sort("foo", 'desc') + }.build() + then: + mapping.sort.name == 'foo' + mapping.sort.direction == 'desc' + + when: + mapping = define { + sort(foo:'bar') + }.build() + + then: + mapping.sort.namesAndDirections == [foo:'bar'] + } + + void "test simple discriminator mapping"() { + given: + Mapping mapping = define { + discriminator "test" + }.build() + + expect: + mapping.discriminator != null + mapping.discriminator.value == 'test' + mapping.discriminator.column == null + mapping.discriminator.insertable == null + } + + void "test complex discriminator mapping"() { + given: + Mapping mapping = define { + discriminator { + value "test" + column { + name "c_test" + } + insertable true + } + }.build() + + expect: + mapping.discriminator != null + mapping.discriminator.value == 'test' + mapping.discriminator.column != null + mapping.discriminator.column.name == 'c_test' + mapping.discriminator.insertable + } + + void "test simple alter version column"() { + given: + Mapping mapping = define { + version "my_version" + }.build() + + expect: + mapping.getPropertyConfig(GormProperties.VERSION).column == "my_version" + } + + void "test complex alter version column"() { + given: + Mapping mapping = define { + version { + type "int" + column { + name 'my_version' + length 10 + } + } + }.build() + PropertyConfig pc = mapping.getPropertyConfig(GormProperties.VERSION) + expect: + pc != null + pc.columns.size() == 1 + pc.type == 'int' + pc.columns[0].length == 10 + pc.column == "my_version" + } + + void "test alter property config using property method"() { + given: + Mapping mapping = define { + property('blah') { + nullable true + column { + defaultValue 'test' + } + } + }.build() + PropertyConfig config = mapping.getPropertyConfig('blah') + + expect: + config != null + config.nullable + config.columns + config.columns[0].defaultValue == 'test' + } + + void "test alter property config using method missing"() { + given: + Mapping mapping = define { + blah = property { + nullable true + column { + defaultValue 'test' + } + } + }.build() + PropertyConfig config = mapping.getPropertyConfig('blah') + + expect: + config != null + config.nullable + config.columns + config.columns[0].defaultValue == 'test' + } + + void "test alter property config using map"() { + given: + Mapping mapping = define { + blah nullable: true,{ + column { + defaultValue 'test' + } + } + }.build() + PropertyConfig config = mapping.getPropertyConfig('blah') + + expect: + config != null + config.nullable + config.columns + config.columns[0].defaultValue == 'test' + } + + void "test configure join table mapping with closure"() { + given: + Mapping mapping = define { + blah = property { + joinTable { + name "foo" + key "foo_id" + column "bar_id" + } + } + }.build() + + PropertyConfig config = mapping.getPropertyConfig('blah') + + expect: + config != null + config.joinTable != null + config.joinTable.name == 'foo' + config.joinTable.key.name == 'foo_id' + config.joinTable.column.name == 'bar_id' + + } + + void "test configure join table mapping with map"() { + given: + Mapping mapping = define { + blah = property { + joinTable name: "foo", + key: "foo_id", + column: "bar_id" + } + }.build() + + PropertyConfig config = mapping.getPropertyConfig('blah') + + expect: + config != null + config.joinTable != null + config.joinTable.name == 'foo' + config.joinTable.key.name == 'foo_id' + config.joinTable.column.name == 'bar_id' + + } + + void "test column config via map"() { + given: + Mapping mapping = define { + table 'myTable' + version false + firstName column:'First_Name', + lazy:true, + unique:true, + type: java.sql.Clob, + length:255, + index:'foo', + sqlType: 'text' + + property('lastName', [column:'Last_Name']) + }.build() + + expect: + "First_Name" == mapping.columns.firstName.column + mapping.columns.firstName.lazy + mapping.columns.firstName.unique + java.sql.Clob == mapping.columns.firstName.type + 255 == mapping.columns.firstName.length + 'foo' == mapping.columns.firstName.getIndexName() + "text" == mapping.columns.firstName.sqlType + "Last_Name" == mapping.columns.lastName.column + } + + void "test global mapping handling"() { + given: + Mapping mapping = define { + '*'(property { + column { + sqlType "text" + } + }) + firstName(property({ + column { + name "test" + } + })) + }.build() + + expect: + mapping.getPropertyConfig('*').sqlType == 'text' + mapping.getPropertyConfig('firstName').sqlType == 'text' + mapping.getPropertyConfig('firstName').column == 'test' + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/AutoTimestampSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/AutoTimestampSpec.groovy new file mode 100644 index 00000000000..12774b904e6 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/AutoTimestampSpec.groovy @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +class AutoTimestampSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([DateCreatedTestA, DateCreatedTestB]) + } + + void "autoTimestamp should prevent custom changes to dateCreated and lastUpdated if turned on"() { + when: "testing insert ignores custom dateCreated and lastUpdated" + def before = new Date() - 5 + def a = new DateCreatedTestA(name: 'David Estes', lastUpdated: before, dateCreated: before) + a.save(flush:true) + a.refresh() + def lastUpdated = a.lastUpdated + def dateCreated = a.dateCreated + + then: + lastUpdated > before + dateCreated > before + + when: "testing update ignores custom dateCreated and lastUpdated" + a.name = "David R. Estes" + a.lastUpdated = before - 5 + a.dateCreated = before - 5 + a.save(flush:true) + a.refresh() + + then: + a.lastUpdated > lastUpdated + a.dateCreated == dateCreated + } + + void "dateCreated and lastUpdated should not be modified by GORM if turned off"() { + when: "insert allows custom dateCreated and lastUpdated" + def now = new Date() + def before = now - 5 + + def a = new DateCreatedTestB(name: 'David Estes', lastUpdated: before, dateCreated: before) + a.save(flush:true) + a.refresh() + + def lastUpdated = a.lastUpdated + def dateCreated = a.dateCreated + + then: + lastUpdated == before + dateCreated == before + + when: "update allows custom dateCreated and lastUpdated" + a.name = "David R. Estes" + a.lastUpdated = now + a.dateCreated = now + a.save(flush:true) + a.refresh() + + then: + a.lastUpdated == now + a.dateCreated == now + } +} + +@Entity +class DateCreatedTestA { + String name + Date dateCreated + Date lastUpdated + + static mapping = { + autoTimestamp true + } +} + +@Entity +class DateCreatedTestB { + String name + Date dateCreated + Date lastUpdated + + static mapping = { + autoTimestamp false + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/BasicCollectionInQuerySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/BasicCollectionInQuerySpec.groovy new file mode 100644 index 00000000000..b5ff16b8b83 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/BasicCollectionInQuerySpec.groovy @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Reproduces https://github.com/apache/grails-core/issues/14610 + * + * When a domain has hasMany to a basic type (String), using 'in' on + * that collection property in criteria queries fails with + * "Parameter #1 is not set". + */ +@Rollback +class BasicCollectionInQuerySpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = + new HibernateDatastore(BcStudent) + + @Issue("https://github.com/apache/grails-core/issues/14610") + def "in query on basic collection type should work"() { + given: + def s1 = new BcStudent(name: "Alice", email: "alice@test.com") + s1.addToSchools("School1") + s1.addToSchools("School2") + s1.save() + + def s2 = new BcStudent(name: "Bob", email: "bob@test.com") + s2.addToSchools("School2") + s2.addToSchools("School3") + s2.save() + + def s3 = new BcStudent(name: "Charlie", email: "charlie@test.com") + s3.addToSchools("School3") + s3.save(flush: true) + + when: + def results = BcStudent.createCriteria().list { + 'in'('schools', ['School2']) + projections { + property 'email' + } + } + + then: + results.sort() == ['alice@test.com', 'bob@test.com'] + } + + def "workaround using createAlias on basic collection"() { + given: + def s1 = new BcStudent(name: "Alice2", email: "alice2@test.com") + s1.addToSchools("SchoolA") + s1.addToSchools("SchoolB") + s1.save() + + def s2 = new BcStudent(name: "Bob2", email: "bob2@test.com") + s2.addToSchools("SchoolB") + s2.save(flush: true) + + when: + def results = BcStudent.createCriteria().list { + createAlias("schools", "s") + 'in'("s.elements", ["SchoolB"]) + projections { + property 'email' + } + } + + then: + results.sort() == ['alice2@test.com', 'bob2@test.com'] + } + + def "multiple in queries on same basic collection should not fail with duplicate alias"() { + given: + def s1 = new BcStudent(name: "Dave", email: "dave@test.com") + s1.addToSchools("MIT") + s1.addToSchools("Harvard") + s1.save() + + def s2 = new BcStudent(name: "Eve", email: "eve@test.com") + s2.addToSchools("Stanford") + s2.addToSchools("Berkeley") + s2.save() + + def s3 = new BcStudent(name: "Frank", email: "frank@test.com") + s3.addToSchools("MIT") + s3.addToSchools("Stanford") + s3.save(flush: true) + + when: + def results = BcStudent.createCriteria().list { + or { + 'in'('schools', ['MIT']) + 'in'('schools', ['Stanford']) + } + projections { + property 'email' + } + } + + then: "all matching students are found (duplicates possible from OR on join table)" + results.unique().sort() == ['dave@test.com', 'eve@test.com', 'frank@test.com'] + } + + def "in query on basic collection with pre-existing alias should reuse it"() { + given: + def s1 = new BcStudent(name: "Grace", email: "grace@test.com") + s1.addToSchools("Yale") + s1.addToSchools("Princeton") + s1.save() + + def s2 = new BcStudent(name: "Hank", email: "hank@test.com") + s2.addToSchools("Princeton") + s2.save() + + def s3 = new BcStudent(name: "Ivy", email: "ivy@test.com") + s3.addToSchools("Columbia") + s3.save(flush: true) + + when: "an alias is explicitly created before using in() with the raw property name" + def results = BcStudent.createCriteria().list { + createAlias("schools", "sch") + 'in'('schools', ['Princeton']) + projections { + property 'email' + } + } + + then: "the existing alias is reused instead of creating a duplicate" + results.sort() == ['grace@test.com', 'hank@test.com'] + } +} + +@Entity +class BcStudent { + String name + String email + + static hasMany = [schools: String] + + static constraints = { + name blank: false + email blank: false + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CascadeToBidirectionalAsssociationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CascadeToBidirectionalAsssociationSpec.groovy new file mode 100644 index 00000000000..356967f83f3 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CascadeToBidirectionalAsssociationSpec.groovy @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests + +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import spock.lang.Issue + +/** + * Created by graemerocher on 01/02/16. + */ +@Issue('https://github.com/apache/grails-core/issues/9290') +class CascadeToBidirectionalAsssociationSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Club, Team, Player, Contract]) + } + + /** + * This test currently fails because the association between Contract and Player is left unassigned + */ + void "test cascades work correctly with a bidirectional association"() { + when: + Club c = new Club(name: "Padres").save() + Team padres = new Team( + name: "Padres 1", + club: c + ) + + + def p = new Player( + name: "John", + contract: new Contract( + salary: 40_000_000 + ) + ) + padres.addToPlayers(p) + + // Desired behavior: Team cascades saves down to Player, which + // cascades its saves down to Contract + padres.save(flush: true) + then: + padres.hasErrors() + padres.errors.getFieldError('players[0].contract.player') + + when:"the contract id is assigned" + p.contract.player = p + padres.save(flush: true) + + then:"The object is saved" + padres.id + + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Club.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Club.groovy new file mode 100644 index 00000000000..d189a3d02b0 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Club.groovy @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests + +import grails.gorm.hibernate.HibernateEntity +import grails.gorm.annotation.Entity + +@Entity +class Club implements HibernateEntity { + String name + + @Override + String toString() { + name + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithJoinTableSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithJoinTableSpec.groovy new file mode 100644 index 00000000000..98cb4487e1d --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithJoinTableSpec.groovy @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests + +import static grails.gorm.hibernate.mapping.MappingBuilder.define + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 26/01/2017. + */ +class CompositeIdWithJoinTableSpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore datastore = new HibernateDatastore(CompositeIdParent, CompositeIdChild) + @Shared PlatformTransactionManager transactionManager = datastore.transactionManager + + @Rollback + void "test composite id with join table"() { + when:"A parent with a composite id and a join table is saved" + new CompositeIdParent(name: "Test" , last:"Test 2") + .addToChildren(new CompositeIdChild()) + .save(flush:true) + + + then:"The entity was saved" + CompositeIdParent.count() == 1 + CompositeIdParent.list().first().children.size() == 1 + } +} + +@Entity +class CompositeIdParent implements Serializable { + String name + String last + static hasMany = [children:CompositeIdChild] + static mapping = define { + id composite('name','last') + property("children") { + joinTable { + name "child_parent" + column "child_id" + } + column { + name "foo" + } + column { + name "bar" + } + } + } +} + +@Entity +class CompositeIdChild { + + static mapping = { + + } + static constraints = { + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy new file mode 100644 index 00000000000..7d7890fbbec --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 26/01/2017. + */ +class CompositeIdWithManyToOneAndSequenceSpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore datastore = new HibernateDatastore(Tooth, ToothDisease) + @Shared PlatformTransactionManager transactionManager = datastore.transactionManager + + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/835') + void "Test composite id many to one and sequence"() { + + when:"a many to one association is created" + ToothDisease td = new ToothDisease(nrVersion: 1).save() + new Tooth(toothDisease: td).save(flush:true) + + then:"The object was saved" + Tooth.count() == 1 + Tooth.list().first().toothDisease != null + } + +} + + +@Entity +class Tooth { + Integer id + ToothDisease toothDisease + static mapping = { + table name: 'AK_TOOTH' + id generator: 'sequence', params: [sequence: 'SEQ_AK_TOOTH'] + toothDisease { + column name: 'FK_AK_TOOTH_ID' + column name: 'FK_AK_TOOTH_NR_VERSION' + } + } +} + +@Entity +class ToothDisease implements Serializable { + Integer idColumn + Integer nrVersion + static mapping = { + table name: 'AK_TOOTH_DISEASE' + idColumn column: 'ID', generator: 'sequence', params: [sequence: 'SEQ_AK_TOOTH_DISEASE'] + nrVersion column: 'NR_VERSION' + id composite: ['idColumn', 'nrVersion'] + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Contract.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Contract.groovy new file mode 100644 index 00000000000..5d59a316e41 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Contract.groovy @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests + +import grails.gorm.annotation.Entity + +/** + * Created by graemerocher on 21/10/16. + */ +@Entity +class Contract { + BigDecimal salary + static belongsTo = [player:Player] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CountByWithEmbeddedSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CountByWithEmbeddedSpec.groovy new file mode 100644 index 00000000000..403d9ccb7ac --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CountByWithEmbeddedSpec.groovy @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import spock.lang.Issue + +/** + * Created by graemerocher on 20/04/16. + */ +class CountByWithEmbeddedSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([CountByPerson]) + } + + @Issue('https://github.com/apache/grails-core/issues/9846') + void "Test countBy query with embedded entity"() { + given: + new CountByPerson(name: "Fred", bornInCountry: new CountByCountry(name: "England")).save(flush: true) + new CountByPerson(bornInCountry: new CountByCountry(name: "Scotland")).save(flush: true) + + expect: + CountByPerson.countByNameIsNotNull() == 1 + } +} + +@Entity +class CountByPerson { + String name + CountByCountry bornInCountry + + static embedded = ['bornInCountry'] + + static constraints = { + name nullable: true + bornInCountry nullable: true + } +} + +class CountByCountry { + String name +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DeleteAllWhereSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DeleteAllWhereSpec.groovy new file mode 100644 index 00000000000..322dbfca02a --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DeleteAllWhereSpec.groovy @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import spock.lang.Issue + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class DeleteAllWhereSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Club]) + } + + @Issue('https://github.com/apache/grails-data-mapping/issues/969') + void "test delete all type conversion"() { + given: + new Club(name: "Manchester United").save() + new Club(name: "Arsenal").save(flush: true) + + when: + int count = Club.count + + then: + count == 2 + + when: + def idList = [Club.findByName("Arsenal").id as Integer] + Club.where { + id in idList + }.deleteAll() + + then: + Club.count == 1 + Club.findByName("Manchester United") + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachCriteriaSubquerySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachCriteriaSubquerySpec.groovy new file mode 100644 index 00000000000..1b98302bf74 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachCriteriaSubquerySpec.groovy @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.DetachedCriteria +import grails.gorm.annotation.Entity +import grails.gorm.hibernate.HibernateEntity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +@SuppressWarnings("GrMethodMayBeStatic") +class DetachCriteriaSubquerySpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([User, Group, GroupAssignment, Organisation]) + } + + void "test detached associated criteria in subquery"() { + + setup: + User supVisor = createUser('supervisor@company.com') + User user1 = createUser('user1@company.com') + User user2 = createUser('user2@company.com') + + Group group1 = createGroup('Group 1', supVisor) + Group group2 = createGroup('Group 2', supVisor) + + assignGroup(user1, group1) + assignGroup(user1, group2) + + when: + String supervisorEmail = 'supervisor@company.com' + DetachedCriteria criteria = User.where { + def u = User + exists( + GroupAssignment.where { + def ga0 = GroupAssignment + user.id == u.id && group.supervisor.email == supervisorEmail + }.id() + ) + } + List result = criteria.list() + + then: + noExceptionThrown() + result.size() == 1 + } + + void "test executing detached criteria in sub-query multiple times"() { + + setup: + Organisation orgA = new Organisation(name: "A") + orgA.addToUsers(email: 'user1@a') + orgA.addToUsers(email: 'user2@a') + orgA.addToUsers(email: 'user3@a') + orgA.save(flush: true) + Organisation orgB = new Organisation(name: "B") + orgB.addToUsers(email: 'user1@b') + orgB.addToUsers(email: 'user2@b') + orgB.save(flush: true) + + when: + DetachedCriteria criteria = User.where { + inList('organisation', Organisation.where { name == 'A' || name == 'B' }.id()) + } + List result = criteria.list() + result = criteria.list() + + then: + result.size() == 5 + } + + void "test that detached criteria subquery should create implicit alias instead of using this_"() { + + setup: + User supVisor = createUser('supervisor@company.com') + User user1 = createUser('user1@company.com') + User user2 = createUser('user2@company.com') + + Group group1 = createGroup('Group 1', supVisor) + Group group2 = createGroup('Group 2', supVisor) + + assignGroup(user1, group1) + assignGroup(user1, group2) + + when: + String supervisorEmail = 'supervisor@company.com' + DetachedCriteria criteria = User.where { + def u = User + exists( + GroupAssignment.where { + user.id == u.id && group.supervisor.email == supervisorEmail + }.id() + ) + } + List result = criteria.list() + + then: + noExceptionThrown() + result.size() == 1 + } + + private User createUser(String email) { + User user = new User(email: email) + Organisation defaultOrg = Organisation.findOrCreateByName("default") + defaultOrg.addToUsers(user) + defaultOrg.save(flush: true) + user + } + + private Group createGroup(String name, User supervisor) { + Group group = new Group() + group.name = name + group.supervisor = supervisor + group.save(flush: true) + } + + private void assignGroup(User user, Group group) { + GroupAssignment groupAssignment = new GroupAssignment() + groupAssignment.user = user + groupAssignment.group = group + groupAssignment.save(flush: true) + } + +} + + +@Entity +class User implements HibernateEntity { + String email + static belongsTo = [organisation: Organisation] + static mapping = { + table 'T_USER' + } +} + +@Entity +class Group implements HibernateEntity { + String name + User supervisor + static mapping = { + table 'T_GROUP' + } +} + +@Entity +class GroupAssignment implements HibernateEntity { + User user + Group group + static mapping = { + table 'T_GROUP_ASSIGNMENT' + } +} + +@Entity +class Organisation implements HibernateEntity { + String name + static hasMany = [users: User] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaJoinSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaJoinSpec.groovy new file mode 100644 index 00000000000..bf174215e94 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaJoinSpec.groovy @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.DetachedCriteria +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.grails.datastore.gorm.finders.DynamicFinder +import org.grails.orm.hibernate.query.HibernateQuery +import org.hibernate.Hibernate + +import jakarta.persistence.criteria.JoinType + +class DetachedCriteriaJoinSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Team, Club, Player, Contract]) + } + + def "check if count works as expected"() { + given: + new Club(name: "Real Madrid").save() + new Club(name: "Barcelona").save() + new Club(name: "Chelsea").save() + new Club(name: "Manchester United").save() + + expect: "max and offset should always be ignored when calling count()" + Club.where {}.max(10).offset(0).count() == 4 + new DetachedCriteria<>(Club).max(10).offset(0).count() == 4 + Club.where {}.max(2).offset(0).count() == 4 + new DetachedCriteria<>(Club).max(2).offset(0).count() == 4 + Club.where {}.max(10).offset(10).count() == 4 + new DetachedCriteria<>(Club).max(10).offset(10).count() == 4 + } + + def 'check if inner join is applied correctly'() { + given: + def dc = new DetachedCriteria(Team).build { + join('club', JoinType.INNER) + createAlias('club', 'c') + } + HibernateQuery query = manager.session.createQuery(Team) + + DynamicFinder.applyDetachedCriteria(query, dc) + def joinType = query.hibernateCriteria.subcriteriaList.first().joinType + expect: + joinType == org.hibernate.sql.JoinType.INNER_JOIN + } + + def 'check if left join is applied correctly'() { + given: + def dc = new DetachedCriteria(Team).build { + join('club', JoinType.LEFT) + createAlias('club', 'c') + } + HibernateQuery query = manager.session.createQuery(Team) + + DynamicFinder.applyDetachedCriteria(query, dc) + def joinType = query.hibernateCriteria.subcriteriaList.first().joinType + expect: + joinType == org.hibernate.sql.JoinType.LEFT_OUTER_JOIN + } + + def 'check if right join is applied correctly'() { + given: + def dc = new DetachedCriteria(Team).build { + join('club', JoinType.RIGHT) + createAlias('club', 'c') + } + HibernateQuery query = manager.session.createQuery(Team) + + DynamicFinder.applyDetachedCriteria(query, dc) + def joinType = query.hibernateCriteria.subcriteriaList.first().joinType + expect: + joinType == org.hibernate.sql.JoinType.RIGHT_OUTER_JOIN + } + + def 'check get honours join and eagerly loads association'() { + given: + def club = new Club(name: 'Juventus').save(flush: true) + new Team(name: 'Torino', club: club).save(flush: true) + + when: + Team team = Team.where { name == 'Torino' }.join('club').get() + + then: + team != null + Hibernate.isInitialized(team.club) + } + + def 'check list with join and projected association property works without explicit alias'() { + given: + def club = new Club(name: 'Milan').save(flush: true) + new Team(name: 'Rossoneri', club: club).save(flush: true) + + when: + def result = Team.where { name == 'Rossoneri' }.join('club').property('club.name').list() + + then: + result == ['Milan'] + } + + def 'check get with join and projected association property works without explicit alias'() { + given: + def club = new Club(name: 'Inter').save(flush: true) + new Team(name: 'Nerazzurri', club: club).save(flush: true) + + when: + def result = Team.where { name == 'Nerazzurri' }.join('club').property('club.name').get() + + then: + result == 'Inter' + } + + def 'check list with association subquery plus join and projection works'() { + given: + def club = new Club(name: 'Ajax').save(flush: true) + new Team(name: 'Amsterdammers', club: club).save(flush: true) + + when: + def result = Team.where { + club { + name == 'Ajax' + } + }.join('club').property('club.name').list() + + then: + result == ['Ajax'] + } + + def 'check list can sort by joined association property'() { + given: + def clubA = new Club(name: 'A Club').save(flush: true) + def clubB = new Club(name: 'B Club').save(flush: true) + new Team(name: 'Team B', club: clubB).save(flush: true) + new Team(name: 'Team A', club: clubA).save(flush: true) + + when: + def result = Team.where {}.join('club').sort('club.name', 'asc').property('name').list() + + then: + result == ['Team A', 'Team B'] + } + + def 'check get honours join with join type and eagerly loads association'() { + given: + def club = new Club(name: 'PSG').save(flush: true) + new Team(name: 'Paris', club: club).save(flush: true) + + when: + Team team = Team.where { name == 'Paris' }.join('club', JoinType.LEFT).get() + + then: + team != null + Hibernate.isInitialized(team.club) + } + + def 'check list with multiple projections on joined association'() { + given: + def club = new Club(name: 'Benfica').save(flush: true) + new Team(name: 'Lisbon', club: club).save(flush: true) + + when: + def result = Team.where { name == 'Lisbon' }.join('club').property('club.name').property('name').list() + + then: + result.size() == 1 + result[0][0] == 'Benfica' + result[0][1] == 'Lisbon' + } + + def 'check list with deep nested projection path on players'() { + given: + def club = new Club(name: 'Boca Juniors').save(flush: true) + def team = new Team(name: 'Xeneizes', club: club).save(flush: true) + def player = new Player(name: 'Roman', team: team) + player.contract = new Contract(salary: 5_000_000G, player: player) + player.save(flush: true) + + when: + def result = Team.where { name == 'Xeneizes' }.join('players').property('players.name').list() + + then: + result == ['Roman'] + } + + def 'check invalid projection path throws exception'() { + when: + new DetachedCriteria(Team).build { + projections { + property('nonexistent.field') + } + }.list() + + then: + thrown(Exception) + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionAliasSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionAliasSpec.groovy new file mode 100644 index 00000000000..d3e86c64558 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionAliasSpec.groovy @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.DetachedCriteria +import grails.gorm.transactions.Rollback +import grails.gorm.transactions.Transactional +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + + +class DetachedCriteriaProjectionAliasSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(Entity1, Entity2, DetachedEntity) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Transactional + def setup() { + DetachedEntity.findAll().each { it.delete() } + Entity1.findAll().each { it.delete(flush: true) } + Entity2.findAll().each { it.delete(flush: true) } + final entity1 = new Entity1(id: 1, field1: 'E1').save() + final entity2 = new Entity2(id: 2, field: 'E2', parent: entity1).save() + entity1.addToChildren(entity2) + new DetachedEntity(id: 1, entityId: entity1.id, field: 'DE1').save() + new DetachedEntity(id: 2, entityId: entity1.id, field: 'DE2').save() + } + + @Rollback + @Issue('https://github.com/grails/grails-data-hibernate5/issues/598') + def 'test projection in detached criteria subquery with aliased join and restriction referencing join'() { + setup: + final detachedCriteria = new DetachedCriteria(Entity1).build { + createAlias("children", "e2") + projections{ + property("id") + } + eq("e2.field", "E2") + } + when: + def res = DetachedEntity.withCriteria { + "in"("entityId", detachedCriteria) + } + then: + res.entityId.first() == 1L + } + + + @Rollback + @Issue('https://github.com/grails/grails-data-hibernate5/issues/598') + def 'test aliased projection in detached criteria subquery'() { + setup: + final detachedCriteria = new DetachedCriteria(Entity2).build { + createAlias("parent", "e1") + projections{ + property("e1.id") + } + eq("field", "E2") + } + when: + def res = DetachedEntity.withCriteria { + "in"("entityId", detachedCriteria) + } + then: + res.entityId.first() == 2L + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionNullAssociationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionNullAssociationSpec.groovy new file mode 100644 index 00000000000..68df6902e91 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionNullAssociationSpec.groovy @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.DetachedCriteria +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import grails.gorm.transactions.Transactional +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Tests that DetachedCriteria projections on nullable association properties + * correctly include rows where the association is null. + */ +class DetachedCriteriaProjectionNullAssociationSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(Shipment, Warehouse) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Transactional + def setup() { + Shipment.findAll().each { it.delete() } + Warehouse.findAll().each { it.delete(flush: true) } + } + + @Rollback + def 'distinct projection on nullable association property includes rows with null association'() { + given: 'shipments with and without a warehouse' + def warehouse = new Warehouse(name: 'Main').save(flush: true) + new Shipment(description: 'With warehouse', warehouse: warehouse).save(flush: true) + new Shipment(description: 'No warehouse', warehouse: null).save(flush: true) + + when: 'projecting distinct warehouse IDs' + def results = new DetachedCriteria(Shipment).build { + projections { + distinct('warehouse.id') + } + }.list() + + then: 'both the warehouse ID and null should be returned' + results.size() == 2 + results.contains(warehouse.id) + results.contains(null) + } + + @Rollback + def 'property projection on nullable association includes rows with null association'() { + given: 'shipments with and without a warehouse' + def warehouse = new Warehouse(name: 'Central').save(flush: true) + new Shipment(description: 'Has warehouse', warehouse: warehouse).save(flush: true) + new Shipment(description: 'Missing warehouse', warehouse: null).save(flush: true) + + when: 'projecting warehouse IDs without distinct' + def results = new DetachedCriteria(Shipment).build { + projections { + property('warehouse.id') + } + }.list() + + then: 'both values should be returned including null' + results.size() == 2 + results.contains(warehouse.id) + results.contains(null) + } + + @Rollback + def 'distinct id with property projection on nullable association includes null rows'() { + given: 'shipments with and without a warehouse' + def warehouse = new Warehouse(name: 'North').save(flush: true) + new Shipment(description: 'Assigned', warehouse: warehouse).save(flush: true) + new Shipment(description: 'Unassigned', warehouse: null).save(flush: true) + + when: 'using distinct on id and property on nullable association' + def results = Shipment.where {}.distinct('id').property('warehouse.id').list() + + then: 'all rows should be returned' + results.size() == 2 + } + + @Rollback + def 'multiple projections with nullable association property preserve null rows'() { + given: 'shipments with and without a warehouse' + def warehouse = new Warehouse(name: 'South').save(flush: true) + new Shipment(description: 'Stored', warehouse: warehouse).save(flush: true) + new Shipment(description: 'In transit', warehouse: null).save(flush: true) + + when: 'projecting both id and nullable warehouse.id' + def results = new DetachedCriteria(Shipment).build { + projections { + property('id') + property('warehouse.id') + } + }.list() + + then: 'both rows should be returned' + results.size() == 2 + def warehouseIds = results.collect { it[1] } + warehouseIds.contains(warehouse.id) + warehouseIds.contains(null) + } +} + +@Entity +class Shipment implements Serializable { + String description + Warehouse warehouse + + static constraints = { + warehouse nullable: true + } +} + +@Entity +class Warehouse implements Serializable { + String name +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionSpec.groovy new file mode 100644 index 00000000000..6a39c87c597 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DetachedCriteriaProjectionSpec.groovy @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.DetachedCriteria +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import grails.gorm.transactions.Transactional +import org.grails.datastore.mapping.query.Query +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 24/10/16. + */ +class DetachedCriteriaProjectionSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(Entity1, Entity2, DetachedEntity) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Transactional + def setup() { + DetachedEntity.findAll().each { it.delete() } + Entity1.findAll().each { it.delete(flush: true) } + final entity1 = new Entity1(id: 1, field1: 'Correct').save() + new Entity1(id: 2, field1: 'Incorrect').save() + new DetachedEntity(id: 1, entityId: entity1.id, field: 'abc').save() + new DetachedEntity(id: 2, entityId: entity1.id, field: 'def').save() + } + + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/792') + def 'closure projection fails'() { + setup: + final detachedCriteria = new DetachedCriteria(DetachedEntity).build { + projections { + distinct 'entityId' + } + eq 'field', 'abc' + } + when: + // will fail + def results = Entity1.withCriteria { + inList 'id', detachedCriteria + } + then: + results.size() == 1 + + } + + @Rollback + def 'closure projection manually'() { + setup: + final detachedCriteria = new DetachedCriteria(DetachedEntity).build { + eq 'field', 'abc' + } + detachedCriteria.projections << new Query.DistinctPropertyProjection('entityId') + expect: + assert Entity1.withCriteria { + inList 'id', detachedCriteria + }.collect { it.field1 }.contains('Correct') + } + + @Rollback + def 'or fails in detached criteria'() { + setup: + final detachedCriteria = new DetachedCriteria(DetachedEntity).build { + or { + eq 'field', 'abc' + eq 'field', 'def' + } + } + detachedCriteria.projections << new Query.DistinctPropertyProjection('entityId') + when: + def results = Entity1.withCriteria { + inList 'id', detachedCriteria + } + then: + results.size() == 1 + } +} + +@Entity +public class Entity1 { + Long id + String field1 + static hasMany = [children : Entity2] +} +@Entity +class Entity2 { + static belongsTo = [parent: Entity1] + String field +} +@Entity +class DetachedEntity { + Long entityId + String field +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DomainGetterSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DomainGetterSpec.groovy new file mode 100644 index 00000000000..22c4cb5f644 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/DomainGetterSpec.groovy @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +/** + * Created by graemerocher on 16/09/2016. + */ +class DomainGetterSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([DomainOne, DomainWithGetter]) + } + + void "test a domain with a getter"() { + when: + new DomainOne(controller: 'project', action: 'update').save(flush: true, validate: false) + + then: + new DomainWithGetter().relatedDomainOne + } +} + +@Entity +class DomainWithGetter { + DomainOne getRelatedDomainOne() { + DomainOne.findByAction("update") + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/EnumMappingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/EnumMappingSpec.groovy new file mode 100644 index 00000000000..15236084400 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/EnumMappingSpec.groovy @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +import java.sql.ResultSet + +/** + * Created by graemerocher on 24/02/16. + */ +class EnumMappingSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Recipe]) + } + + void "Test enum mapping"() { + when: "An enum property is persisted" + new Recipe(title: "Chicken Tikka Masala").save(flush: true) + ResultSet resultSet = manager.sessionFactory.currentSession.connection().prepareStatement("select * from recipe").executeQuery() + resultSet.next() + + then: "The enum is mapped as a varchar" + resultSet.getString('type') == 'GOOD' + + } +} + +@Entity +class Recipe { + String title + RecipeType type = RecipeType.GOOD +} + +enum RecipeType { + GOOD, BAD, BORING +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ExecuteQueryWithinValidatorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ExecuteQueryWithinValidatorSpec.groovy new file mode 100644 index 00000000000..a519280572d --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ExecuteQueryWithinValidatorSpec.groovy @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.Session +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 17/02/2017. + */ +class ExecuteQueryWithinValidatorSpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore hibernateDatastore = new HibernateDatastore(Named, NameType) + + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.transactionManager + + @Rollback + void "test executeQuery method executed during validation"() { + when:"a validator executed an HQL query" + NameType nt = new NameType(nameType: "test").save(flush:true) + Named.withSession { Session session -> + session.save(new Named(nameType: nt)) + } + + + then:"no stackoverflow occurs" + NameType.count() == 1 + Named.count() == 1 + } +} + +@Entity +class Named { + NameType nameType + + static constraints = { + nameType (validator: { val, obj, errors -> + if (val !=null) { + def parms = [nameType: val.nameType.trim().toLowerCase() ] + def rows = NameType.executeQuery("""select nameType from NameType where lower(nameType) = :nameType""", parms) + + def found =false + if (rows !=null && rows.size() ==1) + found =true + if (!found) { + errors.rejectValue("nameType","personNames.nameType.invalidValue") + } + + // handle case-sensitivity if (val.trim() != rows[0]) obj.nametype = rows[0] + } + }) + } +} + +@Entity +class NameType { + String nameType +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateEntityTraitGeneratedSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateEntityTraitGeneratedSpec.groovy new file mode 100644 index 00000000000..5bef5c69803 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateEntityTraitGeneratedSpec.groovy @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.transactions.Rollback +import groovy.transform.Generated +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.IgnoreIf +import spock.lang.Shared +import spock.lang.Specification + +@Rollback +class HibernateEntityTraitGeneratedSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(Club) + + void "test that all HibernateEntity trait methods are marked as Generated"() { + // Unfortunately static methods have to check directly one by one + expect: + Club.getMethod('findAllWithSql', CharSequence).isAnnotationPresent(Generated) + Club.getMethod('findWithSql', CharSequence).isAnnotationPresent(Generated) + Club.getMethod('findAllWithSql', CharSequence, Map).isAnnotationPresent(Generated) + Club.getMethod('findWithSql', CharSequence, Map).isAnnotationPresent(Generated) + } + +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateOptimisticLockingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateOptimisticLockingSpec.groovy new file mode 100644 index 00000000000..42426db154f --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateOptimisticLockingSpec.groovy @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import org.apache.grails.data.testing.tck.domains.OptLockNotVersioned +import org.apache.grails.data.testing.tck.domains.OptLockVersioned +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.grails.orm.hibernate.support.hibernate5.HibernateOptimisticLockingFailureException + +/** + * @author Burt Beckwith + */ +class HibernateOptimisticLockingSpec extends GrailsDataTckSpec { + + void "Test optimistic locking"() { + + given: + def o = new OptLockVersioned(name: 'locked').save(flush: true) + manager.session.clear() + manager.transactionManager.commit manager.transactionStatus + manager.transactionStatus = null + + when: + OptLockVersioned.withTransaction { + o = OptLockVersioned.get(o.id) + + Thread.start { + OptLockVersioned.withTransaction { s -> + def reloaded = OptLockVersioned.get(o.id) + assert reloaded + assert reloaded != o + reloaded.name += ' in new session' + reloaded.save(flush: true) + assert reloaded.version == 1 + assert o.version == 0 + } + + }.join() + + o.name += ' in main session' + o.save(flush: true) + + manager.session.clear() + o = OptLockVersioned.get(o.id) + } + then: + thrown HibernateOptimisticLockingFailureException + } + + void "Test optimistic locking disabled with 'version false'"() { + given: + def o = new OptLockNotVersioned(name: 'locked').save(flush: true) + manager.session.clear() + manager.transactionManager.commit manager.transactionStatus + manager.transactionStatus = null + + when: + def ex + OptLockNotVersioned.withTransaction { + o = OptLockNotVersioned.get(o.id) + + Thread.start { + OptLockNotVersioned.withTransaction { s -> + def reloaded = OptLockNotVersioned.get(o.id) + reloaded.name += ' in new session' + reloaded.save(flush: true) + } + + }.join() + + o.name += ' in main session' + + try { + o.save(flush: true) + } + catch (e) { + ex = e + e.printStackTrace() + } + + manager.session.clear() + o = OptLockNotVersioned.get(o.id) + + } + + then: + ex == null + o.name == 'locked in main session' + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateSuite.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateSuite.groovy new file mode 100644 index 00000000000..32f8cec6285 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateSuite.groovy @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import org.apache.grails.data.testing.tck.tests.FirstAndLastMethodSpec +import org.junit.platform.suite.api.SelectClasses +import org.junit.platform.suite.api.Suite + +/** + * Created by graemerocher on 06/07/2016. + */ +@Suite +@SelectClasses([FirstAndLastMethodSpec]) +class HibernateSuite { +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateValidationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateValidationSpec.groovy new file mode 100644 index 00000000000..02ceec66d63 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateValidationSpec.groovy @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import org.apache.grails.data.testing.tck.domains.ChildEntity +import org.apache.grails.data.testing.tck.domains.ClassWithListArgBeforeValidate +import org.apache.grails.data.testing.tck.domains.ClassWithNoArgBeforeValidate +import org.apache.grails.data.testing.tck.domains.ClassWithOverloadedBeforeValidate +import org.apache.grails.data.testing.tck.domains.TestEntity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.springframework.transaction.support.TransactionSynchronizationManager + +/** + * Tests validation semantics. + */ +class HibernateValidationSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses += [ClassWithListArgBeforeValidate, ClassWithNoArgBeforeValidate, + ClassWithOverloadedBeforeValidate] + } + + void "Test that validate works without a bound Session"() { + given: + def t + + when: + manager.session.disconnect() + def resource + if (TransactionSynchronizationManager.hasResource(manager.session.datastore.sessionFactory)) { + resource = TransactionSynchronizationManager.unbindResource(manager.session.datastore.sessionFactory) + } + + t = new TestEntity(name:"") + + then: + TransactionSynchronizationManager.getResource(manager.session.datastore.sessionFactory) == null + t.save() == null + t.hasErrors() == true + + when: + TransactionSynchronizationManager.bindResource(manager.session.datastore.sessionFactory, resource) + + then: + 1 == t.errors.allErrors.size() + 0 == TestEntity.count() + + when: + t.clearErrors() + t.name = "Bob" + t.age = 45 + t.child = new ChildEntity(name:"Fred") + t = t.save(flush: true) + + then: + t != null + 1 == TestEntity.count() + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy new file mode 100644 index 00000000000..47b6afdb710 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import javax.sql.DataSource +import java.sql.ResultSet + +/** + * Created by graemerocher on 16/11/16. + */ +class IdentityEnumTypeSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(EnumEntityDomain, FooWithEnum) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.getTransactionManager() + + @Rollback + void "test identity enum type"() { + when: + new EnumEntityDomain(status: EnumEntityDomain.Status.FOO).save(flush:true) + DataSource ds = hibernateDatastore.connectionSources.defaultConnectionSource.dataSource + ResultSet resultSet = ds.getConnection().prepareStatement('select status from enum_entity_domain').executeQuery() + + then: + resultSet.next() + resultSet.getString(1) == 'F' + EnumEntityDomain.first().status == EnumEntityDomain.Status.FOO + } + + @Rollback + void "test identity enum type 2"() { + when: + new FooWithEnum(name: "blah", mySuperValue: XEnum.X__TWO).save(flush:true) + DataSource ds = hibernateDatastore.connectionSources.defaultConnectionSource.dataSource + ResultSet resultSet = ds.getConnection().prepareStatement('select my_super_value from foo_with_enum').executeQuery() + + then: + resultSet.next() + resultSet.getInt(1) == 100 + FooWithEnum.first().mySuperValue == XEnum.X__TWO + } +} + +@Entity +class EnumEntityDomain { + Status status + + static mapping = { + status(enumType: "identity") + } + + enum Status { + FOO("F"), BAR("B") + String id + Status(String id) { this.id = id } + } +} + +@Entity +class FooWithEnum { + long id + String name + XEnum mySuperValue + + static mapping = { + version false + mySuperValue enumType:"identity" + } +} + +enum XEnum { + X__ONE (000, "x.one"), + X__TWO (100, "x.two"), + X__THREE (200, "x.three") + + final int id + final String name + + private XEnum(int id, String name) { + this.id = id + this.name = name + } + + String toString() { + name + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ImportFromConstraintSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ImportFromConstraintSpec.groovy new file mode 100644 index 00000000000..79be465e681 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ImportFromConstraintSpec.groovy @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 20/10/16. + */ +class ImportFromConstraintSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(TestA, TestB) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Rollback + void "test regular mas size constraints"() { + when:"An entity is saved that validates the max size constraint" + def result = new TestB(name: "12345678").save() + + then:"The entity was not saved" + result == null + TestB.count == 0 + + when:"An entity is saved and validation bypassed" + new TestB(name: "12345678").save(validate:false, flush:true) + + then:"A constraint violation is thrown" + thrown(DataIntegrityViolationException) + } + + @Rollback + void "test importFrom mas size constraints"() { + when:"An entity is saved that validates the max size constraint" + def result = new TestA(name: "12345678").save() + + then:"The entity was not saved" + result == null + TestA.count == 0 + + when:"An entity is saved and validation bypassed" + new TestA(name: "12345678").save(validate:false, flush:true) + + then:"A constraint violation is thrown" + thrown(DataIntegrityViolationException) + } +} + +@Entity +class TestB { + + String name + + static constraints = { + name (nullable: true, maxSize: 6) + } +} +@Entity +class TestA { + + String name + + static constraints = { + importFrom(TestB) + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/LastUpdateWithDynamicUpdateSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/LastUpdateWithDynamicUpdateSpec.groovy new file mode 100644 index 00000000000..6d8f65d6fa8 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/LastUpdateWithDynamicUpdateSpec.groovy @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +/** + * Created by graemerocher on 27/06/16. + */ +class LastUpdateWithDynamicUpdateSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([LastUpdateTestA, LastUpdateTestB, LastUpdateTestC]) + } + + void "lastUpdated should work for dynamic update and no versioning on TestA"() { + given: + def a = new LastUpdateTestA(name: 'David Estes') + a.save(flush:true) + a.refresh() + def lastUpdated = a.lastUpdated + sleep(5) + when: + a.name = "David R. Estes" + a.save(flush:true) + a.refresh() + then: + a.lastUpdated > lastUpdated + } + + void "lastUpdated should work for dynamic update with version true TestB"() { + given: + def a = new LastUpdateTestB(name: 'David Estes') + a.save(flush:true) + a.refresh() + def lastUpdated = a.lastUpdated + sleep(5) + when: + a.name = "David R. Estes" + a.save(flush:true) + a.refresh() + then: + a.lastUpdated > lastUpdated + } + + void "lastUpdated should work for dynamic update false and versioning on TestC"() { + given: + def a = new LastUpdateTestC(name: 'David Estes') + a.save(flush:true) + a.refresh() + def lastUpdated = a.lastUpdated + sleep(5) + when: + a.name = "David R. Estes" + a.save(flush:true) + a.refresh() + then: + a.lastUpdated > lastUpdated + } + + + void "autoTimestamp should work with updateAll for dynamic update false and versioning on TestC"() { + given: + def a = new LastUpdateTestC(name: 'David Estes') + a.save(flush:true) + a.refresh() + def lastUpdated = a.lastUpdated + sleep(5) + when: + LastUpdateTestC.where{ + eq 'id', a.id + }.updateAll(name: 'David R. Estes') + a.refresh() + then: + a.lastUpdated > lastUpdated + } +} + + +@Entity +class LastUpdateTestA { + + String name + Date dateCreated + Date lastUpdated + + static mapping = { + version false + dynamicUpdate true + } +} + +@Entity +class LastUpdateTestB { + String name + Date dateCreated + Date lastUpdated + + static mapping = { + version true + dynamicUpdate true + } +} + +@Entity +class LastUpdateTestC { + String name + Date dateCreated + Date lastUpdated + + static mapping = { + version true + dynamicUpdate false + } + static constraints = { + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ManyToOneSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ManyToOneSpec.groovy new file mode 100644 index 00000000000..505bfdd0e14 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ManyToOneSpec.groovy @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +/** + * Created by graemerocher on 27/06/16. + */ +class ManyToOneSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Foo, Bar]) + } + + static { + System.setProperty("org.jboss.logging.provider", "slf4j") + } + + void "Test many-to-one association"() { + when: "A many-to-one association is saved" + Foo foo1 = new Foo(fooDesc: "Foo One").save() + Foo foo2 = new Foo(fooDesc: "Foo Two").save() + Foo foo3 = new Foo(fooDesc: "Foo Three").save() + + foo3.bar = new Bar(barDesc: "Bar Three", foo: foo3) + foo3.save(flush: true) + foo1.bar = new Bar(barDesc: "Bar One", foo: foo1) + foo1.save(flush: true) + foo2.bar = new Bar(barDesc: "Bar Two", foo: foo2) + foo2.save(flush: true) + + manager.session.clear() + println "RETRIEVING FOOS!" + def foos = Foo.findAll() + println("Foos:") + foos.each { f -> + println(f.fooDesc + " -> " + f.bar.barDesc) + } + + manager.session.clear() + + println "RETRIEVING BARS!" + def bars = Bar.findAll() + println("Bars:") + bars.each { b -> + println(b.barDesc + " -> " + b.foo.fooDesc) + } + manager.session.clear() + + foo1 = Foo.get(foo1.id) + foo2 = Foo.get(foo2.id) + foo3 = Foo.get(foo3.id) + + + Bar bar1 = Bar.findByBarDesc("Bar One") + Bar bar2 = Bar.findByBarDesc("Bar Two") + Bar bar3 = Bar.findByBarDesc("Bar Three") + + then: "The data model is correct" + foo1.fooDesc == "Foo One" + foo1.bar.barDesc == "Bar One" + foo2.fooDesc == "Foo Two" + foo2.bar.barDesc == "Bar Two" + foo3.fooDesc == "Foo Three" + foo3.bar.barDesc == "Bar Three" + bar1.barDesc == "Bar One" + bar1.foo.fooDesc == "Foo One" + bar2.barDesc == "Bar Two" + bar2.foo.fooDesc == "Foo Two" + bar3.barDesc == "Bar Three" + bar3.foo.fooDesc == "Foo Three" + } +} + +@Entity +class Foo { + + String fooDesc + + Bar bar + + static mapping = { + id generator: 'identity' + } + + static constraints = { + bar(nullable: true) + } +} + +@Entity +class Bar { + + String barDesc + + static belongsTo = [foo: Foo] + + static mapping = { + id generator: 'identity' + } + + static constraints = { + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/MultiColumnUniqueConstraintSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/MultiColumnUniqueConstraintSpec.groovy new file mode 100644 index 00000000000..635b802f1b0 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/MultiColumnUniqueConstraintSpec.groovy @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.springframework.dao.DataIntegrityViolationException +import spock.lang.Issue + +@Issue('https://github.com/apache/grails-data-mapping/issues/617') +class MultiColumnUniqueConstraintSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([DomainOne, Task1, TaskLink]) + } + + void "test generated unique constraints"() { + expect: + new DomainOne(controller: 'project', action: 'update').save(flush:true) + new DomainOne(controller: 'project', action: 'delete').save(flush:true) + new DomainOne(controller: 'projectTask', action: 'update').save(flush:true) + } + + void "test generated unique constraints violation"() { + when: + new DomainOne(controller: 'project', action: 'update').save(flush:true) + new DomainOne(controller: 'project', action: 'update').save(flush:true, validate:false) + + then: + thrown DataIntegrityViolationException + } + + void "test generated unique constraints for related domains"() { + given: 'two existing tasks' + Task1 task1 = new Task1(name: 'task1').save(flush: true, failOnError: true) + Task1 task2 = new Task1(name: 'task2').save(flush: true, failOnError: true) + + when: 'saving task links for the same toTask but not breaking unique index' + TaskLink taskLink1 = new TaskLink(fromTask: task1, toTask: task2).save(flush: true, validate: false) + TaskLink taskLink2 = new TaskLink(fromTask: task2, toTask: task2).save(flush: true, validate: false) + + then: 'both links may be saved' + taskLink1 + taskLink2 + + when: 'instance which breaks unique index is saved' + new TaskLink(fromTask: task1, toTask: task2).save(flush: true, validate: false) + + then: 'DataIntegrityViolationException is thrown' + thrown DataIntegrityViolationException + } +} + +@Entity +class DomainOne { + + String controller + String action + + static constraints = { + action unique: 'controller' + } +} + + +@Entity +class Task1 { + String name +} + +@Entity +class TaskLink { + + Task1 toTask + Task1 fromTask + + static constraints = { + toTask unique: ['fromTask'] + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/NullableAndLengthSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/NullableAndLengthSpec.groovy new file mode 100644 index 00000000000..67c276e0558 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/NullableAndLengthSpec.groovy @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 20/10/16. + */ +class NullableAndLengthSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(Node) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/10107') + void "Test nullable and length mapping"() { + when:"An object is persisted that violates the length mapping" + new Node(label: "AAAAAAAAAAAA").save(flush:true) + + then:"An exception was thrown" + thrown(DataIntegrityViolationException) + } + +} +@Entity +class Node { + String label + + static constraints = { + label nullable: true + } + + static mapping = { + label length: 6 + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Player.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Player.groovy new file mode 100644 index 00000000000..d932487ba6a --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Player.groovy @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity + +/** + * Created by graemerocher on 21/10/16. + */ +@Entity +class Player { + String name + static belongsTo = [team:Team] + static hasOne = [contract:Contract] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/RLikeSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/RLikeSpec.groovy new file mode 100644 index 00000000000..2b8c354af46 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/RLikeSpec.groovy @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +class RLikeSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([RlikeFoo]) + } + + void "test rlike works with H2"() { + given: + new RlikeFoo(name: "ABC").save(flush: true) + new RlikeFoo(name: "ABCDEF").save(flush: true) + new RlikeFoo(name: "ABCDEFGHI").save(flush: true) + + when: + manager.session.clear() + List allFoos = RlikeFoo.findAllByNameRlike("ABCD.*") + + then: + allFoos.size() == 2 + } +} + +@Entity +class RlikeFoo { + String name +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ReadOperationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ReadOperationSpec.groovy new file mode 100644 index 00000000000..55e133bc034 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ReadOperationSpec.groovy @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import org.apache.grails.data.testing.tck.domains.TestEntity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +class ReadOperationSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([TestEntity]) + } + + void "test read operation for non existent"() { + expect: + TestEntity.read(10) == null + } + + void "test read operation"() { + given: + TestEntity te = new TestEntity(name: "bob") + te.save(flush:true) + + expect: + TestEntity.count() == 1 + TestEntity.read(te.id) != null + TestEntity.exists(te.id) + !TestEntity.exists(10) + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SaveWithExistingValidationErrorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SaveWithExistingValidationErrorSpec.groovy new file mode 100644 index 00000000000..a7bf77b4806 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SaveWithExistingValidationErrorSpec.groovy @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 21/10/16. + */ +class SaveWithExistingValidationErrorSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(ObjectA, ObjectB) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/9820') + void "test saving an object with another invalid object"() { + when:"An object with a validation error is assigned" + def testB = new ObjectB() + testB.save(flush: true) //fails because name is not nullable + + def testA = new ObjectA(test: testB) + testA.save(flush: true) + + then:"Neither objects were saved" + ObjectA.count == 0 + ObjectB.count == 0 + testA.errors.getFieldError("test.name") + } + +} +@Entity +class ObjectA { + + ObjectB test + + static constraints = { + } +} +@Entity +class ObjectB { + + String name + + static constraints = { + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SchemaNameSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SchemaNameSpec.groovy new file mode 100644 index 00000000000..c581080c11e --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SchemaNameSpec.groovy @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.grails.orm.hibernate.cfg.Settings +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 20/10/16. + */ +class SchemaNameSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(['dataSource.url':'jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000;INIT=create schema if not exists myschema', (Settings.SETTING_DB_CREATE):'create-drop'],CustomSchema) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/10083') + void 'test schema name alteration with h2'() { + when:"An object with a custom schema is saved" + new CustomSchema(name: "Test").save(flush:true) + + then:"The object was persisted" + CustomSchema.count() == 1 + } + + +} +@Entity +class CustomSchema { + String name + static mapping = { + table schema:'myschema' + } +} + + diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SequenceIdSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SequenceIdSpec.groovy new file mode 100644 index 00000000000..10e822d857c --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SequenceIdSpec.groovy @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.engine.spi.SessionImplementor +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + + +/** + * Created by graemerocher on 20/10/16. + */ +class SequenceIdSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(BookWithSequence) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Rollback + void "test sequence generator"() { + when:"A book is saved" + BookWithSequence book = new BookWithSequence(title: 'The Stand') + book.save(flush:true) + + then:"The entity was saved" + BookWithSequence.first() + + ((SessionImplementor)datastore.sessionFactory.currentSession).connection().prepareStatement("call NEXT VALUE FOR book_seq;") + .executeQuery() + .next() + } +} +@Entity +class BookWithSequence { + String title + + static constraints = { + } + + static mapping = { + version false + id generator:'sequence', params:[sequence:'book_seq'] + id index:'book_id_idx' + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SizeConstraintSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SizeConstraintSpec.groovy new file mode 100644 index 00000000000..de394fed020 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SizeConstraintSpec.groovy @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.testing.tck.domains.TestEntity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.springframework.dao.DataIntegrityViolationException +import spock.lang.Issue + +/** + * Created by graemerocher on 25/01/2017. + */ +class SizeConstraintSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([SizeConstrainedUser]) + } + + @Issue('https://github.com/apache/grails-data-mapping/issues/846') + void "test size constraint is used in schema"() { + when:"A constraint is violated" + new SizeConstrainedUser(username:"blah", columnAa:"123456", columnBb:"123456").save(flush:true, validate:false) + + then:"an exception is thrown" + thrown(DataIntegrityViolationException) + + when:"A constraint is violated" + new SizeConstrainedUser(username:"blah", columnAa:"123456", columnBb:"12345").save(flush:true, validate:false) + + then:"an exception is thrown" + thrown(DataIntegrityViolationException) + + when:"A constraints are not violated" + manager.session.clear() + new SizeConstrainedUser(username:"blah", columnAa:"12345", columnBb:"12345").save(flush:true, validate:false) + + then:"the insert occurred" + SizeConstrainedUser.count() == 1 + + } +} + +@Entity +class SizeConstrainedUser { + String username + String columnAa + String columnBb + + static constraints = { + username(blank: false) + columnAa(nullable: true, size: 0..5) + columnBb(nullable: true, maxSize: 5) + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SqlQuerySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SqlQuerySpec.groovy new file mode 100644 index 00000000000..8616f767785 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SqlQuerySpec.groovy @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.IgnoreIf +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 17/11/16. + */ +@Rollback +class SqlQuerySpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(Club) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + void "test simple query returns a single result"() { + given: + setupTestData() + + when:"Some test data is saved" + String name = "Arsenal" + Club c = Club.findWithSql("select * from club c where c.name = $name") + + then:"The results are correct" + c != null + c.name == name + + } + + void "test simple sql query"() { + + given: + setupTestData() + + when:"Some test data is saved" + List results = Club.findAllWithSql("select * from club c order by c.name") + + then:"The results are correct" + results.size() == 3 + results[0] instanceof Club + results[0].name == 'Arsenal' + } + + void "test sql query with gstring parameters"() { + given: + setupTestData() + + when:"Some test data is saved" + String p = "%l%" + List results = Club.findAllWithSql("select * from club c where c.name like $p order by c.name") + + then:"The results are correct" + results.size() == 2 + results[0] instanceof Club + results[0].name == 'Arsenal' + } + + void "test escape HQL in findAll with gstring"() { + given: + setupTestData() + + when:"A query is used that embeds a GString with a value that should be encoded for the query to succeed" + String p = "%l%" + List results = Club.findAll("from Club c where c.name like $p order by c.name") + + then:"The results are correct" + results.size() == 2 + results[0] instanceof Club + results[0].name == 'Arsenal' + + when:"A query that passes arguments is used" + results = Club.findAll("from Club c where c.name like $p and c.name like :test order by c.name", [test:'%e%']) + + then:"The results are correct" + results.size() == 2 + results[0] instanceof Club + results[0].name == 'Arsenal' + } + + void "test escape HQL in executeQuery with gstring"() { + given: + setupTestData() + + when:"A query is used that embeds a GString with a value that should be encoded for the query to succeed" + String p = "%l%" + List results = Club.executeQuery("from Club c where c.name like $p order by c.name") + + then:"The results are correct" + results.size() == 2 + results[0] instanceof Club + results[0].name == 'Arsenal' + + when:"A query that passes arguments is used" + results = Club.executeQuery("from Club c where c.name like $p and c.name like :test order by c.name", [test:'%e%']) + + then:"The results are correct" + results.size() == 2 + results[0] instanceof Club + results[0].name == 'Arsenal' + } + + void "test escape HQL in find with gstring"() { + given: + setupTestData() + + when:"A query is used that embeds a GString with a value that should be encoded for the query to succeed" + String p = "%chester%" + Club c = Club.find("from Club c where c.name like $p order by c.name") + + then:"The results are correct" + c != null + c.name == 'Manchester United' + + when:"A query that passes arguments is used" + c = Club.find("from Club c where c.name like $p and c.name like :test order by c.name", [test:'%e%']) + + then:"The results are correct" + c != null + c.name == 'Manchester United' + } + + protected void setupTestData() { + new Club(name: "Barcelona").save() + new Club(name: "Arsenal").save() + new Club(name: "Manchester United").save(flush: true) + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SubclassMultipleListCollectionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SubclassMultipleListCollectionSpec.groovy new file mode 100644 index 00000000000..e7a2f710862 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SubclassMultipleListCollectionSpec.groovy @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.* + +/** + * Created by graemerocher on 01/03/2017. + */ +@Ignore +class SubclassMultipleListCollectionSpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore hibernateDatastore + @Shared PlatformTransactionManager transactionManager + + + void setupSpec() { + hibernateDatastore = new HibernateDatastore( + SuperProduct, Product, Iteration + ) + transactionManager = hibernateDatastore.getTransactionManager() + } + + @Ignore // not yet implemented + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/882') + void "test inheritance with multiple list collections"() { + when: + Iteration iter = new Iteration() + iter.addToProducts(new Product()) + iter.addToOtherProducts(new SuperProduct()) + iter.save(flush:true) + + then: + Iteration.count == 1 + } +} + +@Entity +class Iteration { + List products + + static hasMany = [products: Product, otherProducts: SuperProduct] + // uncommenting this line resolves the issue +// static mappedBy = [products: 'iteration', otherProducts: 'none'] +} + +@Entity +class Product extends SuperProduct { + + static belongsTo = [iteration: Iteration] +} + +@Entity +class SuperProduct { + + static constraints = { + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SubqueryAliasSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SubqueryAliasSpec.groovy new file mode 100644 index 00000000000..5361555c7f6 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SubqueryAliasSpec.groovy @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.transactions.Rollback +import org.grails.datastore.gorm.query.transform.ApplyDetachedCriteriaTransform +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 01/03/2017. + */ +@ApplyDetachedCriteriaTransform +class SubqueryAliasSpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore datastore = new HibernateDatastore( + Club, Team + ) + + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Rollback + void "Test subquery with root alias"() { + given: + Club c = new Club(name: "Manchester United").save() + new Team(name: "First Team", club: c).save(flush:true) + + when: + Team t = Team.where { + def t = Team + name == "First Team" + exists(Club.where { + id == t.club + }.property('name')) + + }.find() + + then: + t != null + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TablePerSubClassAndEmbeddedSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TablePerSubClassAndEmbeddedSpec.groovy new file mode 100644 index 00000000000..572e1c89534 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TablePerSubClassAndEmbeddedSpec.groovy @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.DetachedCriteria +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria +import org.grails.datastore.gorm.query.transform.ApplyDetachedCriteriaTransform +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 04/11/16. + */ +@ApplyDetachedCriteriaTransform +class TablePerSubClassAndEmbeddedSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(Company, Vendor) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.getTransactionManager() + + @Rollback + void 'test table per subclass with embedded entity'() { + given:"some test data" + Vendor vendor = new Vendor(name: "Blah") + vendor.address = new Address(address: "somewhere", city: "Youngstown", state: "OH", zip: "44555") + vendor.save(failOnError:true, flush:true) + + when:"a query executed" + def results = Vendor.where { +// like 'address.zip', '%44%' ? + address.zip =~ '%44%' + }.list(max: 10, offset: 0) + + then:"the results are correct" + results.size() == 1 + } + + void "test transform query with embedded entity"() { + when:"A query is parsed that queries the embedded entity" + def gcl = new GroovyClassLoader() + DetachedCriteria criteria = gcl.parseClass(''' +import grails.gorm.tests.* + +Vendor.where { + address.zip =~ '%44%' + name == 'blah' +} +''').newInstance().run() + + then:"The criteria contains the correct criterion" + criteria.criteria[0] instanceof DetachedAssociationCriteria + criteria.criteria[0].association.name == 'address' + criteria.criteria[0].criteria[0].property == 'zip' + } +} + + +@Entity +class Company { + Address address + String name + + static embedded = ['address'] + static constraints = { + address nullable: true + } + static mapping = { + tablePerSubclass true + } +} +@Entity +class Vendor extends Company { + + static constraints = { + } +} +class Address { + String address + String city + String state + String zip +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Team.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Team.groovy new file mode 100644 index 00000000000..06ff79711e3 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Team.groovy @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import groovy.transform.ToString + +/** + * Created by graemerocher on 21/10/16. + */ +@Entity +@ToString(includes = 'name') +class Team { + Club club + String name + static hasMany = [players: Player] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ToOneProxySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ToOneProxySpec.groovy new file mode 100644 index 00000000000..50b312f9bd8 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ToOneProxySpec.groovy @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.grails.orm.hibernate.proxy.HibernateProxyHandler + +/** + * Created by graemerocher on 16/12/16. + */ +class ToOneProxySpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Team, Club]) + } + + void "test that a proxy is not initialized on get"() { + given: + Team t = new Team(name: "First Team", club: new Club(name: "Manchester United").save()) + t.save(flush:true) + manager.session.clear() + + + when:"An object is retrieved and the session is flushed" + t = Team.get(t.id) + manager.session.flush() + + def proxyHandler = new HibernateProxyHandler() + then:"The association was not initialized" + proxyHandler.getAssociationProxy(t, "club") != null + !proxyHandler.isInitialized(t, "club") + + + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TwoBidirectionalOneToManySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TwoBidirectionalOneToManySpec.groovy new file mode 100644 index 00000000000..ad3ff9a7553 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TwoBidirectionalOneToManySpec.groovy @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 26/01/2017. + */ +class TwoBidirectionalOneToManySpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore datastore = new HibernateDatastore(Room, PointX, PointY) + @Shared PlatformTransactionManager transactionManager = datastore.transactionManager + + @Rollback + void "test an entity with 2 bidirectional one-to-many mappings"() { + when:"A new entity is created is created" + Room r = new Room(name:"Test") + .addToPointx(new PointX()) + .addToPointy(new PointY()) + + r.save(flush:true) + + then:"The entity was saved" + !r.errors.hasErrors() + Room.count == 1 + } +} + +@Entity +class Room { + static hasMany = [pointx:PointX,pointy:PointY] + + String name +} + +@Entity +class PointX { + static belongsTo = [room:Room] + Room destiny + static constraints = { + destiny nullable:true + } +} + +@Entity +class PointY { + static belongsTo = [room:Room] + Room destiny + static constraints = { + destiny nullable:true + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueConstraintHibernateSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueConstraintHibernateSpec.groovy new file mode 100644 index 00000000000..d5f1f01303b --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueConstraintHibernateSpec.groovy @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import org.apache.grails.data.testing.tck.domains.GroupWithin +import org.apache.grails.data.testing.tck.domains.UniqueGroup +import org.grails.datastore.gorm.GormEntity +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Tests the unique constraint + */ +/** + * + * NOTE: This test is disabled because in order for the test suite to run quickly we need to run each test in a transaction. + * This makes it not possible to test the scenario outlined here, however tests for this use case exist in the hibernate plugin itself + * so we are covered. + * + */ +class UniqueConstraintHibernateSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(UniqueGroup, GroupWithin, Driver, License) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.getTransactionManager() + + void "Test simple unique constraint"() { + when:"Two domain classes with the same name are saved" + UniqueGroup one = UniqueGroup.withTransaction { + new UniqueGroup(name:"foo").save(flush:true) + } + + + UniqueGroup two = UniqueGroup.withTransaction { + def ug = new UniqueGroup(name: "foo") + ug.save(flush:true) + return ug + } + + + then:"The second has errors" + two.hasErrors() + UniqueGroup.withTransaction { UniqueGroup.count() } == 1 + + when:"The first is saved again" + one = UniqueGroup.withTransaction { + def ug = UniqueGroup.findByName("foo") + ug.save(flush:true) + return ug + } + + then:"The are no errors" + one != null + + when:"Three domain classes are saved within different uniqueness groups" + GroupWithin group1 + GroupWithin group2 + GroupWithin group3 + GroupWithin.withTransaction { + group1 = new GroupWithin(name:"foo", org:"mycompany").save(flush:true) + group2 = new GroupWithin(name:"foo", org:"othercompany").save(flush:true) + group3 = new GroupWithin(name:"foo", org:"mycompany") + group3.save(flush:true) + + } + + then:"Only the third has errors" + one != null + two != null + group3.hasErrors() + GroupWithin.withTransaction { GroupWithin.count() } == 2 + + } + + @spock.lang.Ignore + def "Test unique constraint with a hasOne association"() { + when:"Two domain classes with the same license are saved" + Driver one + Driver two + License license + Driver.withTransaction { + license = new License() + def driver = new Driver(license: license) + driver.license = license + one = driver.save(flush: true) + two = new Driver(license: license) + two.license = license + two.save(flush: true) + } + + then:"The second has errors" + one != null + two.hasErrors() + Driver.withTransaction { Driver.count() } == 1 + Driver.withTransaction { License.count() } == 1 + + when:"The first is saved again" + one = Driver.withTransaction { + Driver d = Driver.findByLicense(license) + d.save(flush:true) + return d + } + + then:"The are no errors" + one != null + } + +} + +@Entity +class Driver implements Serializable { + Long id + Long version + static hasOne = [license: License] + License license + static constraints = { + license unique: true + } +} + +@Entity +class License implements GormEntity { + Long id + Long version + Driver driver +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueWithMultipleDataSourcesSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueWithMultipleDataSourcesSpec.groovy new file mode 100644 index 00000000000..09862a5fabe --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueWithMultipleDataSourcesSpec.groovy @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.core.connections.ConnectionSource +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.dialect.H2Dialect +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 17/02/2017. + */ +class UniqueWithMultipleDataSourcesSpec extends Specification { + + @Shared Map config = [ + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.cache':['use_second_level_cache':true,'region.factory_class':'org.hibernate.cache.ehcache.EhCacheRegionFactory'], + 'hibernate.hbm2ddl.auto': 'create', + 'dataSources.second':[url:"jdbc:h2:mem:second;LOCK_TIMEOUT=10000"], + ] + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config),Abc) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.transactionManager + + @Rollback + @Ignore + @Issue('https://github.com/apache/grails-core/issues/10481') + void "test multiple data sources and unique constraint"() { + when: + Abc abc = new Abc(temp: "testing") + abc.save() + + Abc abc1 = new Abc(temp: "testing") + Abc.second.withNewSession{ + abc1.second.save() + } + + then: + !abc1.hasErrors() + } +} + +@Entity +class Abc { + + String temp + + static constraints = { + temp unique: true + } + + static mapping = { + datasource(ConnectionSource.ALL) + } +} + diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryBugFixSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryBugFixSpec.groovy new file mode 100644 index 00000000000..f5f6858e015 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryBugFixSpec.groovy @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.hibernate.HibernateEntity +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +import grails.gorm.transactions.Rollback + +import jakarta.persistence.criteria.JoinType + +/** + * Tests for where-query bug fixes in PR 2. + */ +class WhereQueryBugFixSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore( + WqAuthor, WqBookItem + ) + @Shared PlatformTransactionManager transactionManager = datastore.transactionManager + + @Rollback + @Issue("https://github.com/apache/grails-core/issues/14485") + def "#14485 - LEFT JOIN in DetachedCriteria subquery should not be downgraded to INNER JOIN"() { + given: "authors with and without books" + def authorWithBooks = new WqAuthor(name: 'Author A').save(flush: true, failOnError: true) + def authorNoBooks = new WqAuthor(name: 'Author B').save(flush: true, failOnError: true) + def authorWithBio = new WqAuthor(name: 'Author C').save(flush: true, failOnError: true) + new WqBookItem(title: 'Novel', wqAuthor: authorWithBooks).save(flush: true, failOnError: true) + new WqBookItem(title: 'Biography', wqAuthor: authorWithBio).save(flush: true, failOnError: true) + + when: "querying authors using a subquery with LEFT JOIN on books" + def subquery = WqAuthor.where { + join('wqBookItems', JoinType.LEFT) + wqBookItems { + or { + isNull('title') + ilike('title', '%biography%') + } + } + }.id() + + def results = WqAuthor.where { + 'in'('id', subquery) + }.list() + + then: "both authors without books (NULL from LEFT JOIN) and with biography are found" + results.size() == 2 + results*.name.sort() == ['Author B', 'Author C'] + } + + @Rollback + @Issue("https://github.com/apache/grails-core/issues/14485") + def "#14485 - direct LEFT JOIN where query returns authors without books"() { + given: "authors with and without books" + def authorWithBooks = new WqAuthor(name: 'Writer A').save(flush: true, failOnError: true) + def authorNoBooks = new WqAuthor(name: 'Writer B').save(flush: true, failOnError: true) + new WqBookItem(title: 'A Novel', wqAuthor: authorWithBooks).save(flush: true, failOnError: true) + + when: "querying with LEFT JOIN directly (not as subquery)" + def results = WqAuthor.where { + join('wqBookItems', JoinType.LEFT) + wqBookItems { + isNull('title') + } + }.list() + + then: "author without books is found via LEFT JOIN null match" + results.size() == 1 + results[0].name == 'Writer B' + } +} + +@Entity +class WqAuthor implements HibernateEntity { + String name + static hasMany = [wqBookItems: WqBookItem] +} + +@Entity +class WqBookItem implements HibernateEntity { + String title + static belongsTo = [wqAuthor: WqAuthor] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryOldIssueVerificationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryOldIssueVerificationSpec.groovy new file mode 100644 index 00000000000..a53128760d9 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryOldIssueVerificationSpec.groovy @@ -0,0 +1,371 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import grails.gorm.annotation.Entity +import grails.gorm.hibernate.HibernateEntity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Verification tests for old where-query issues reported against GORM 6.x / Grails 3.x. + * These tests confirm whether each issue has been fixed in the current 7.x codebase. + * + * Issues verified: + * - #14596: where-query returning wrong result if expression not assigned to variable + * - #14622: where-query with multi-level association restriction produces wrong result + * - #14480: countByStuff() not working with where queries + * - #11202: where queries in tests not filtering results + * - #14636: many-to-many queries with sorting raise exception + * - #14610: error querying association with basic collection types + * - #14569: count() incorrect with projection in where query + * - #14600: findAllBy* in bidirectional hasMany produces error + */ +class WhereQueryOldIssueVerificationSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore( + WqFoo, WqWord, WqPhrase, WqSentence, + WqScientificBook, WqBookAuthor, + WqThing, + WqUserRole, WqRoleUser, WqRole, + WqStudent, + WqGroupedItem, + WqBiBook, WqBiAuthor + ) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/14596') + def "where-query returning wrong result if expression not assigned to variable"() { + given: "a Foo with bar set to a non-null value" + new WqFoo(bar: "something").save(flush: true) + + when: "querying inline for records where bar == null" + long inlineCount = WqFoo.where { bar == null }.count() + + and: "querying with variable assignment for records where bar == null" + def criteria = WqFoo.where { bar == null } + long variableCount = criteria.count() + + then: "both should return 0 since no Foo has bar == null" + inlineCount == 0 + variableCount == 0 + } + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/14596') + def "where-query inline vs variable produces same result with matching records"() { + given: "Foos with null and non-null bar values" + new WqFoo(bar: null).save(flush: true) + new WqFoo(bar: "something").save(flush: true) + + when: "querying inline" + long inlineCount = WqFoo.where { bar == null }.count() + + and: "querying with variable" + def criteria = WqFoo.where { bar == null } + long variableCount = criteria.count() + + then: "both should return 1" + inlineCount == 1 + variableCount == 1 + } + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/14622') + def "where-query with multi-level association restriction produces correct result"() { + given: "a sentence -> phrase -> word hierarchy" + def sentence = new WqSentence(text: "Hello World").save(flush: true) + def phrase1 = new WqPhrase(text: "Hello", sentence: sentence).save(flush: true) + def phrase2 = new WqPhrase(text: "World", sentence: sentence).save(flush: true) + def word1 = new WqWord(text: "Hel", phrase: phrase1).save(flush: true) + def word2 = new WqWord(text: "lo", phrase: phrase1).save(flush: true) + def word3 = new WqWord(text: "Wor", phrase: phrase2).save(flush: true) + def word4 = new WqWord(text: "ld", phrase: phrase2).save(flush: true) + + when: "querying words by sentence via multi-level association" + def words = WqWord.where { + phrase.sentence == sentence + }.list() + + then: "all 4 words belonging to the sentence should be returned" + words.size() == 4 + } + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/14480') + def "countBy dynamic finder works correctly with where queries"() { + given: "scientific and non-scientific books by different authors" + def author1 = new WqBookAuthor(name: "Author A").save(flush: true) + def author2 = new WqBookAuthor(name: "Author B").save(flush: true) + new WqScientificBook(title: "Science 1", scientific: true, author: author1).save(flush: true) + new WqScientificBook(title: "Science 2", scientific: true, author: author1).save(flush: true) + new WqScientificBook(title: "Novel 1", scientific: false, author: author1).save(flush: true) + new WqScientificBook(title: "Science 3", scientific: true, author: author2).save(flush: true) + + when: "using countByAuthor on a where query filtering scientific books" + def scientificBooks = WqScientificBook.where { scientific == true } + long findCount = scientificBooks.findAllByAuthor(author1).size() + long countResult = scientificBooks.countByAuthor(author1) + + then: "countByAuthor should match findAllByAuthor count" + findCount == 2 + countResult == 2 + } + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/11202') + def "where queries in tests filter results correctly"() { + given: "two things with different names" + new WqThing(name: "thing 1").save(flush: true) + new WqThing(name: "thing 2").save(flush: true) + + when: "querying with where inline" + def inlineResult = WqThing.where { name == "thing 1" }.list() + + then: "where query filters correctly" + inlineResult.size() == 1 + + when: "querying from a closure" + def queryClosure = { -> WqThing.where { name == "thing 1" } } + def closureResult = queryClosure.call().list() + + then: "closure-based where query filters correctly" + closureResult.size() == 1 + } + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/14636') + def "many-to-many queries with sorting do not throw exception"() { + given: "users and roles in a many-to-many relationship" + def role1 = new WqRole(name: "ADMIN").save(flush: true) + def role2 = new WqRole(name: "USER").save(flush: true) + def user1 = new WqRoleUser(username: "alice").save(flush: true) + def user2 = new WqRoleUser(username: "bob").save(flush: true) + new WqUserRole(user: user1, role: role1).save(flush: true) + new WqUserRole(user: user2, role: role2).save(flush: true) + new WqUserRole(user: user1, role: role2).save(flush: true) + + when: "querying UserRole by role and sorting by user.username" + def results = WqUserRole.where { + role == role1 + }.list(sort: 'user.username') + + then: "no exception is thrown and results are correct" + noExceptionThrown() + results.size() == 1 + results[0].user.username == "alice" + } + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/14610') + def "querying association with basic collection types works"() { + given: "students with basic collection type (hasMany String)" + def s1 = new WqStudent(name: "Alice", email: "alice@test.com").save(flush: true) + s1.addToSchools("School1") + s1.addToSchools("School2") + s1.save(flush: true) + + def s2 = new WqStudent(name: "Bob", email: "bob@test.com").save(flush: true) + s2.addToSchools("School2") + s2.addToSchools("School3") + s2.save(flush: true) + + when: "querying students by school using criteria" + def emails = WqStudent.createCriteria().list { + 'in'('schools', ['School1']) + projections { + property 'email' + } + } + + then: "the query works without error" + noExceptionThrown() + emails.contains("alice@test.com") + } + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/14569') + def "count() gives correct results with projection in where query"() { + given: "items with different groupings" + (1..12).each { new WqGroupedItem(itemGroup: 1, itemValue: "a${it}").save() } + (1..16).each { new WqGroupedItem(itemGroup: 2, itemValue: "b${it}").save() } + (1..9).each { new WqGroupedItem(itemGroup: 3, itemValue: "c${it}").save() } + (1..18).each { new WqGroupedItem(itemGroup: 4, itemValue: "d${it}").save() } + (1..5).each { new WqGroupedItem(itemGroup: 5, itemValue: "e${it}").save(flush: true) } + + when: "creating a where query with groupProperty and count projections" + def c = WqGroupedItem.where { + projections { + groupProperty 'itemGroup' + count() + } + } + def groups = c.list() + + then: "list returns the correct number of groups" + groups.size() == 5 + + and: "count returns the number of groups, not the count from first projection row" + c.count() == 5 + } + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/14600') + def "findAllBy works with bidirectional hasMany relation"() { + given: "authors with books in a bidirectional hasMany" + def author1 = new WqBiAuthor(name: "Stephen King").save(flush: true) + def book1 = new WqBiBook(title: "IT").save(flush: true) + def book2 = new WqBiBook(title: "The Shining").save(flush: true) + author1.addToBooks(book1) + author1.addToBooks(book2) + book1.addToAuthors(author1) + book2.addToAuthors(author1) + author1.save(flush: true) + + when: "using withCriteria to find books by author" + def books = WqBiBook.withCriteria { + authors { + 'in'('id', [author1.id]) + } + } + + then: "books are found without error" + noExceptionThrown() + books.size() == 2 + } +} + + +@Entity +class WqFoo implements HibernateEntity { + String bar + + static constraints = { + bar nullable: true + } +} + +@Entity +class WqSentence implements HibernateEntity { + String text +} + +@Entity +class WqPhrase implements HibernateEntity { + String text + static belongsTo = [sentence: WqSentence] +} + +@Entity +class WqWord implements HibernateEntity { + String text + static belongsTo = [phrase: WqPhrase] +} + +@Entity +class WqScientificBook implements HibernateEntity { + String title + Boolean scientific + + static belongsTo = [author: WqBookAuthor] +} + +@Entity +class WqBookAuthor implements HibernateEntity { + String name + static hasMany = [books: WqScientificBook] +} + +@Entity +class WqThing implements HibernateEntity { + String name +} + +@Entity +class WqRole implements HibernateEntity { + String name +} + +@Entity +class WqRoleUser implements HibernateEntity { + String username + static hasMany = [userRoles: WqUserRole] +} + +@Entity +class WqUserRole implements HibernateEntity, Serializable { + static belongsTo = [user: WqRoleUser, role: WqRole] + + static mapping = { + id composite: ['user', 'role'] + } + + static constraints = { + user unique: 'role' + } +} + +@Entity +class WqStudent implements HibernateEntity { + String name + String email + + static hasMany = [schools: String] + + static mapping = { + schools joinTable: [column: 'school'] + } + + static constraints = { + name blank: false + email blank: false + } +} + +@Entity +class WqGroupedItem implements HibernateEntity { + Integer itemGroup + String itemValue + + static mapping = { + itemGroup column: 'item_group' + itemValue column: 'item_value' + } +} + +@Entity +class WqBiBook implements HibernateEntity { + String title + + static hasMany = [authors: WqBiAuthor] + static belongsTo = WqBiAuthor +} + +@Entity +class WqBiAuthor implements HibernateEntity { + String name + + static hasMany = [books: WqBiBook] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryWithAssociationSortSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryWithAssociationSortSpec.groovy new file mode 100644 index 00000000000..fc8652b58ae --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WhereQueryWithAssociationSortSpec.groovy @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.hibernate.QueryException +import spock.lang.Issue + +/** + * Created by graemerocher on 03/11/16. + */ +class WhereQueryWithAssociationSortSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Club, Team]) + } + + @Issue('https://github.com/apache/grails-core/issues/9860') + void "Test sort with where query that queries association"() { + given:"some test data" + def c = new Club(name: "Manchester United").save() + def t = new Team(club: c, name: "MU First Team").save() + def c2 = new Club(name: "Arsenal").save() + def t2 = new Team(club: c2, name: "Arsenal First Team").save(flush:true) + + when:"a where query uses a sort on an association" + def results = Team.where { + club.name == "Manchester United" + }.list(sort:'club.name') + + + then:"an exception is thrown because no alias is specified" + thrown QueryException + + + when:"a where query uses a sort on an association" + results = Team.where { + def c1 = club + c1.name ==~ '%e%' + }.list(sort:'c1.name') + + + then:"an exception is thrown because no alias is specified" + results.size() == 2 + results.first().name == "Arsenal First Team" + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WithNewSessionAndExistingTransactionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WithNewSessionAndExistingTransactionSpec.groovy new file mode 100644 index 00000000000..002d8e00aea --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WithNewSessionAndExistingTransactionSpec.groovy @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests + +import org.apache.grails.data.testing.tck.domains.Book +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.Session +import org.grails.orm.hibernate.support.hibernate5.SessionHolder +import org.springframework.transaction.TransactionStatus +import org.springframework.transaction.support.TransactionSynchronizationManager +import spock.lang.Issue + +import javax.sql.DataSource + +/** + * Created by graemerocher on 26/08/2016. + */ +class WithNewSessionAndExistingTransactionSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Book]) + } + + void "Test withNewSession when an existing transaction is present"() { + when:"An existing transaction not to pick up the current session" + manager.sessionFactory.currentSession + SessionHolder previousSessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory) + Book.withNewSession { Session session -> + // access the current session + assert !previousSessionHolder.is(TransactionSynchronizationManager.getResource(manager.sessionFactory)) + session.sessionFactory.currentSession + } + // reproduce session closed problem + int result = Book.count() + SessionHolder sessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory) + DataSource dataSource = ((HibernateDatastore)manager.session.datastore).connectionSources.defaultConnectionSource.dataSource + org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = dataSource.targetDataSource.targetDataSource + + then:"The result is correct" + dataSource != null + tomcatDataSource != null + tomcatDataSource.pool.active == 1 + sessionHolder.is(previousSessionHolder) + TransactionSynchronizationManager.isSynchronizationActive() + sessionHolder.session.isOpen() + sessionHolder.isSynchronizedWithTransaction() + manager.sessionFactory.currentSession.isOpen() + result == 0 + Book.count() == 0 + manager.sessionFactory.currentSession == manager.hibernateSession + manager.hibernateSession.isOpen() + } + + @Issue('https://github.com/apache/grails-core/issues/10426') + void "Test with withNewSession with nested transaction"() { + when:"An existing transaction not to pick up the current session" + manager.sessionFactory.currentSession + SessionHolder previousSessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory) + Book.withNewSession { Session session -> + assert !previousSessionHolder.is(TransactionSynchronizationManager.getResource(manager.sessionFactory)) + // access the current session + session.sessionFactory.currentSession + // reproduce "Pre-bound JDBC Connection found!" problem + Book.withNewTransaction { + assert !previousSessionHolder.is(TransactionSynchronizationManager.getResource(manager.sessionFactory)) + new Book(title: "The Stand", author: 'Stephen King').save() + } + } + + Book.count() + SessionHolder sessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory) + + DataSource dataSource = ((HibernateDatastore)manager.session.datastore).connectionSources.defaultConnectionSource.dataSource + org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = dataSource.targetDataSource.targetDataSource + + then:"The result is correct" + dataSource != null + tomcatDataSource != null + tomcatDataSource.pool.active == 1 + sessionHolder.is(previousSessionHolder) + TransactionSynchronizationManager.isSynchronizationActive() + sessionHolder.session.isOpen() + sessionHolder.isSynchronizedWithTransaction() + manager.sessionFactory.currentSession.isOpen() + manager.sessionFactory.currentSession == manager.hibernateSession + manager.hibernateSession.isOpen() + } + + @Issue('https://github.com/apache/grails-core/issues/10448') + void "Test with withNewSession with existing transaction"() { + + when:"the connection pool is obtained" + DataSource dataSource = ((HibernateDatastore)manager.session.datastore).connectionSources.defaultConnectionSource.dataSource + org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = dataSource.targetDataSource.targetDataSource + + then:"the active count is correct" + dataSource != null + tomcatDataSource != null + tomcatDataSource.pool.active == 0 + + when:"An existing transaction not to pick up the current session" + manager.sessionFactory.currentSession + SessionHolder previousSessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory) + Book.withNewTransaction { TransactionStatus status -> + // reproduce "java.lang.IllegalStateException: No value for key" problem + Book.withNewSession { Session session -> + // access the current session + assert !previousSessionHolder.is(TransactionSynchronizationManager.getResource(manager.sessionFactory)) + session.sessionFactory.currentSession + + new Book(title: "The Stand", author: 'Stephen King').save() + } + } + + SessionHolder sessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory) + + + then:"After withNewSession is completed all connections are closed" + tomcatDataSource.pool.active == 0 + + when:"A count is executed that uses the current connection" + Book.count() + + then:"The result is correct" + tomcatDataSource.pool.active == 1 + sessionHolder.is(previousSessionHolder) + TransactionSynchronizationManager.isSynchronizationActive() + sessionHolder.session.isOpen() + sessionHolder.isSynchronizedWithTransaction() + manager.sessionFactory.currentSession.isOpen() + manager.sessionFactory.currentSession == manager.hibernateSession + manager.hibernateSession.isOpen() + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/autoimport/AutoImportSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/autoimport/AutoImportSpec.groovy new file mode 100644 index 00000000000..1ff89355242 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/autoimport/AutoImportSpec.groovy @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests.autoimport + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +class AutoImportSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([A, grails.gorm.tests.autoimport.other.A]) + } + + void "test a domain with a getter"() { + when: + new A().save(flush: true, validate: false) + + then: + noExceptionThrown() + } +} + +@Entity +class A { + +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/autoimport/other/A.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/autoimport/other/A.groovy new file mode 100644 index 00000000000..a4c7795c402 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/autoimport/other/A.groovy @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests.autoimport.other + +import grails.persistence.Entity + +@Entity +class A { + + static mapping = { + autoImport false + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/BidirectionalOneToOneWithUniqueSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/BidirectionalOneToOneWithUniqueSpec.groovy new file mode 100644 index 00000000000..0b01f2d0fcd --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/BidirectionalOneToOneWithUniqueSpec.groovy @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests.belongsto + +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +/** + * Created by graemerocher on 22/08/2017. + */ +class BidirectionalOneToOneWithUniqueSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([HibernateFace, HibernateNose]) + } + + void "test bidirectional one-to-one with unique"() { + + given: + def nose = new HibernateNose() + def face = new HibernateFace(nose: nose) + nose.face = face + face.save(flush: true) + manager.session.clear() + + when: + HibernateFace f = HibernateFace.first() + + then: + f.nose + f.nose.face + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/HibernateFace.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/HibernateFace.groovy new file mode 100644 index 00000000000..32f84d3697b --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/HibernateFace.groovy @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.belongsto + +import grails.gorm.annotation.Entity + +/** + * Created by graemerocher on 22/08/2017. + */ +@Entity +class HibernateFace { + HibernateNose nose + static constraints = { + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/HibernateNose.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/HibernateNose.groovy new file mode 100644 index 00000000000..691b24a8dda --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/belongsto/HibernateNose.groovy @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.belongsto + +import grails.gorm.annotation.Entity + +/** + * Created by graemerocher on 22/08/2017. + */ +@Entity +class HibernateNose { + static belongsTo = [face: HibernateFace] + static constraints = { + face unique: true + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdCriteria.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdCriteria.groovy new file mode 100644 index 00000000000..3c249eb20ee --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdCriteria.groovy @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests.compositeid + +import grails.gorm.annotation.Entity +import grails.gorm.hibernate.mapping.MappingBuilder +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +@Rollback +class CompositeIdCriteria extends Specification { + + @Shared + @AutoCleanup + HibernateDatastore datastore = new HibernateDatastore(CompositeIdToMany, CompositeIdSimple, Author, Book) + + @Issue("https://github.com/grails/grails-data-hibernate5/issues/234") + def "test that composite to-many properties can be queried using JPA"() { + Author _author = new Author(name:"Author").save() + Book _book = new Book(title:"Book").save() + CompositeIdToMany compositeIdToMany = new CompositeIdToMany(author:_author, book:_book).save(failOnError:true, flush:true) + + def criteriaBuilder = datastore.sessionFactory.criteriaBuilder + def criteriaQuery = criteriaBuilder.createQuery() + def root = criteriaQuery.from(CompositeIdToMany) + criteriaQuery.select(root) + criteriaQuery.where(criteriaBuilder.equal(root.get("author"), _author)) + def query = datastore.sessionFactory.currentSession.createQuery(criteriaQuery) + + expect: + query.list() == [compositeIdToMany] + } + + def "test that composite can be queried using JPA"() { + CompositeIdSimple compositeIdSimple = new CompositeIdSimple(name:"name", age:2l).save(failOnError:true, flush:true) + + def criteriaBuilder = datastore.sessionFactory.criteriaBuilder + def criteriaQuery = criteriaBuilder.createQuery() + def root = criteriaQuery.from(CompositeIdSimple) + criteriaQuery.select(root) + criteriaQuery.where(criteriaBuilder.equal(root.get("name"), "name")) + def query = datastore.sessionFactory.currentSession.createQuery(criteriaQuery) + + expect: + query.list() == [compositeIdSimple] + } + + @Issue("https://github.com/apache/grails-data-mapping/issues/1351") + def "test that composite to-many can be used in criteria"() { + Author _author = new Author(name:"Author").save() + Book _book = new Book(title:"Book").save() + CompositeIdToMany compositeIdToMany = new CompositeIdToMany(author:_author, book:_book).save(failOnError:true, flush:true) + + expect: + CompositeIdToMany.createCriteria().list { + author { + eq('id', _author.id) + } + } == [compositeIdToMany] + } + @Issue("https://github.com/apache/grails-core/issues/14516") + def "test that composite id components can be used in criteria projections"() { + Author _author = new Author(name:"Author").save() + Book _book = new Book(title:"Book").save() + CompositeIdToMany compositeIdToMany = new CompositeIdToMany(author:_author, book:_book).save(failOnError:true, flush:true) + + when: "querying with projections navigating composite ID component associations" + def results = CompositeIdToMany.createCriteria().list { + projections { + book { + property('id') + } + } + author { + eq('id', _author.id) + } + } + + then: "the projection returns the expected ID" + results.size() == 1 + results[0] == _book.id + } + + @Issue("https://github.com/apache/grails-core/issues/14516") + def "test that composite id components can be used in criteria restrictions"() { + Author _author = new Author(name:"Author2").save() + Book _book = new Book(title:"Book2").save() + CompositeIdToMany compositeIdToMany = new CompositeIdToMany(author:_author, book:_book).save(failOnError:true, flush:true) + + when: "querying with restrictions on composite ID component associations" + def results = CompositeIdToMany.createCriteria().list { + author { + eq('name', 'Author2') + } + book { + eq('title', 'Book2') + } + } + + then: "the entity is found" + results.size() == 1 + results[0] == compositeIdToMany + } + + @Issue("https://github.com/apache/grails-core/issues/14516") + def "test that eq on composite id component entity works"() { + Author _author = new Author(name:"Author3").save() + Book _book = new Book(title:"Book3").save() + CompositeIdToMany compositeIdToMany = new CompositeIdToMany(author:_author, book:_book).save(failOnError:true, flush:true) + + when: "querying with eq on composite ID component association" + def results = CompositeIdToMany.createCriteria().list { + eq('author', _author) + } + + then: "the entity is found" + results.size() == 1 + results[0] == compositeIdToMany + } + + @Issue("https://github.com/apache/grails-core/issues/14516") + def "test that eq on composite id component works with Hibernate proxy"() { + given: "an entity with composite ID saved and session cleared" + Author _author = new Author(name:"ProxyAuthor").save(flush:true) + Book _book = new Book(title:"ProxyBook").save(flush:true) + CompositeIdToMany compositeIdToMany = new CompositeIdToMany(author:_author, book:_book).save(failOnError:true, flush:true) + def authorId = _author.id + datastore.sessionFactory.currentSession.clear() + + when: "querying with eq using a Hibernate proxy (uninitialized) for the composite ID component" + Author proxyAuthor = Author.load(authorId) + def results = CompositeIdToMany.createCriteria().list { + eq('author', proxyAuthor) + } + + then: "the entity is found via the proxy's ID without initializing it" + results.size() == 1 + results[0].author.id == authorId + results[0].book.title == "ProxyBook" + } +} + +@Entity +class Author { + String name +} + +@Entity +class Book { + String title +} + +@Entity +class CompositeIdToMany implements Serializable { + Author author + Book book + + static mapping = MappingBuilder.define { + composite("author", "book") + } +} + +@Entity +class CompositeIdSimple implements Serializable { + String name + Long age + + static mapping = MappingBuilder.define { + composite("name", "age") + } +} + + diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdWithDeepOneToManyMappingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdWithDeepOneToManyMappingSpec.groovy new file mode 100644 index 00000000000..0aadde525da --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdWithDeepOneToManyMappingSpec.groovy @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests.compositeid + +import grails.gorm.annotation.Entity +import grails.gorm.hibernate.mapping.MappingBuilder +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 26/01/2017. + */ +class CompositeIdWithDeepOneToManyMappingSpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore datastore = new HibernateDatastore(GrandParent, Parent, Child) + @Shared PlatformTransactionManager transactionManager = datastore.transactionManager + + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/660') + void 'test composite id with nested one-to-many mappings'() { + when: + def grandParent = new GrandParent(luckyNumber: 7, name: "Fred") + def parent = new Parent(name: "Bob") + grandParent.addToParents(parent) + parent.addToChildren(name:"Chuck") + grandParent.save(flush:true) + + then: + Parent.count == 1 + GrandParent.count == 1 + Child.count == 1 + GrandParent.list().first().parents.first().children.first().parent != null + } +} + +@Entity +class Child implements Serializable { + String name + + static belongsTo= [parent: Parent] + + static mapping = MappingBuilder.define { + composite('parent', 'name') + } +} + +@Entity +class Parent implements Serializable { + String name + Collection children + + static belongsTo= [grandParent: GrandParent] + static hasMany= [children: Child] + + static mapping= MappingBuilder.define { + composite('grandParent', 'name') + } +} + +@Entity +class GrandParent implements Serializable { + String name + Integer luckyNumber + Collection parents + + static hasMany= [parents: Parent] + + static mapping= MappingBuilder.define { + composite('name', 'luckyNumber') + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/GlobalConstraintWithCompositeIdSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/GlobalConstraintWithCompositeIdSpec.groovy new file mode 100644 index 00000000000..6a3cf26b9de --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/compositeid/GlobalConstraintWithCompositeIdSpec.groovy @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.compositeid + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.orm.hibernate.HibernateDatastore +import org.grails.orm.hibernate.cfg.PropertyConfig +import org.hibernate.dialect.H2Dialect +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 17/02/2017. + */ +class GlobalConstraintWithCompositeIdSpec extends Specification { + + @Shared Map config = [ + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'grails.gorm.default.constraints':{ + '*'(nullable: true) + } + ] + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config),ParentB, ChildB, DomainB) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.transactionManager + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/10457') + void "test global constraints with composite id"() { + when: + ParentB parent = new ParentB(code:"AAA", desc: "BBB") + .addToChilds(name:"Child A") + .save(flush:true) + + then: + ParentB.count == 1 + ChildB.count == 1 + } + + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/877') + void "test global constraints with unique constraint"() { + given: + PersistentEntity entity = hibernateDatastore.mappingContext.getPersistentEntity(DomainB.name) + PropertyConfig nameProp = entity.getPropertyByName('name').mapping.mappedForm + PropertyConfig someOtherConfig = entity.getPropertyByName('someOther').mapping.mappedForm + expect: + nameProp.unique + someOtherConfig.unique + !nameProp.uniquenessGroup.isEmpty() + nameProp.uniquenessGroup.contains('domainB') + someOtherConfig.uniquenessGroup.isEmpty() + + } +} + + +@Entity +class ParentB implements Serializable { + + String code + String desc + + static hasMany = [childs: ChildB] + + static constraints = { + } + + static mapping = { + id composite: ['code', 'desc'] + + code column: 'COD' + desc column: 'DSC' + } +} + +@Entity +class ChildB implements Serializable { + String name + + static belongsTo = [parent: ParentB] + + static constraints = { + } + + static mapping = { + id composite: ['name', 'parent'] + + columns { + parent { + column name: 'COD' + column name: 'DSC' + } + } + } +} + +@Entity +class DomainB { + + String name + + String someOther + + static belongsTo = [domainB: DomainB] + + static constraints = { + name nullable: false, blank: false, unique: "domainB" + someOther nullable: false, blank: false, unique: true + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateDirtyCheckingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateDirtyCheckingSpec.groovy new file mode 100644 index 00000000000..11813a361f0 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateDirtyCheckingSpec.groovy @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.dirtychecking + +import grails.gorm.annotation.Entity +import grails.gorm.dirty.checking.DirtyCheck +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 03/05/2017. + */ +class HibernateDirtyCheckingSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(Person) + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/10613') + void "Test that presence of beforeInsert doesn't impact dirty properties"() { + given: 'a new person' + def person = new Person(name: 'John', occupation: 'Grails developer').save(flush:true) + + when: 'the name is changed' + person.name = 'Dave' + + then: 'the name field is dirty' + person.getPersistentValue('name') == "John" + person.dirtyPropertyNames.contains 'name' + person.dirtyPropertyNames == ['name'] + person.isDirty('name') + !person.isDirty('occupation') + + when: + person.save(flush:true) + + then: + person.getPersistentValue('name') == "Dave" + person.dirtyPropertyNames == [] + !person.isDirty('name') + !person.isDirty() + + when: + person.occupation = "Civil Engineer" + + then: + person.getPersistentValue('occupation') == "Grails developer" + person.dirtyPropertyNames.contains 'occupation' + person.dirtyPropertyNames == ['occupation'] + person.isDirty('occupation') + !person.isDirty('name') + } + + @Rollback + void "test dirty checking on embedded"() { + given: 'a new person' + Person person = new Person(name: 'John', occupation: 'Grails developer', address: new Address(street: "Old Town", zip: "1234")).save(flush:true) + + when: 'the name is changed' + person.address.street = "New Town" + + then: + person.address.hasChanged() + person.address.hasChanged("street") + + when: + person.save(flush:true) + + then: + !person.address.hasChanged() + person.address.listDirtyPropertyNames().isEmpty() + + when: + hibernateDatastore.sessionFactory.currentSession.clear() + person = Person.first() + + then: + person.address.street == "New Town" + } + + @Rollback + void "test dirty checking on boolean true -> false"() { + given: 'a new person' + new Person(name: 'John', occupation: 'Grails developer', employed: true).save(flush: true) + hibernateDatastore.sessionFactory.currentSession.clear() + Person person = Person.first() + + when: + person.employed = false + + then: + person.getPersistentValue('employed') == true + person.dirtyPropertyNames == ['employed'] + person.isDirty('employed') + + when: + person.save(flush:true) + hibernateDatastore.sessionFactory.currentSession.clear() + person = Person.first() + + then: + person.employed == false + } + + @Rollback + void "test dirty checking on boolean false -> true"() { + given: 'a new person' + new Person(name: 'John', occupation: 'Grails developer', employed: false).save(flush: true) + hibernateDatastore.sessionFactory.currentSession.clear() + Person person = Person.first() + + when: + person.employed = true + + then: + person.getPersistentValue('employed') == false + person.dirtyPropertyNames == ['employed'] + person.isDirty('employed') + + when: + person.save(flush:true) + hibernateDatastore.sessionFactory.currentSession.clear() + person = Person.first() + + then: + person.employed == true + } + +} + + +@Entity +class Person { + + String name + String occupation + boolean employed + + Address address + static embedded = ['address'] + + static constraints = { + address nullable:true + } + + def beforeInsert() { + // Do nothing + } +} + +@DirtyCheck +class Address { + String street + String zip +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateUpdateFromListenerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateUpdateFromListenerSpec.groovy new file mode 100644 index 00000000000..58fc8936b01 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateUpdateFromListenerSpec.groovy @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.dirtychecking + +import grails.gorm.transactions.Rollback +import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener +import org.grails.datastore.mapping.engine.event.PreInsertEvent +import org.grails.datastore.mapping.engine.event.PreUpdateEvent +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.context.ApplicationEvent +import org.springframework.context.ApplicationEventPublisher +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class HibernateUpdateFromListenerSpec extends Specification { + + @Shared + @AutoCleanup + HibernateDatastore datastore = new HibernateDatastore(Person) + @Shared PlatformTransactionManager transactionManager = datastore.transactionManager + + PersonSaveOrUpdatePersistentEventListener listener + + void setup() { + listener = new PersonSaveOrUpdatePersistentEventListener(datastore) + ApplicationEventPublisher publisher = datastore.applicationEventPublisher + if (publisher instanceof ConfigurableApplicationEventPublisher) { + ((ConfigurableApplicationEventPublisher) publisher).addApplicationListener(listener) + } else if (publisher instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) publisher).addApplicationListener(listener) + } + } + + @Rollback + void "test the changes made from the listener are saved"() { + when: + Person danny = new Person(name: "Danny", occupation: "manager").save() + + then: + new PollingConditions().eventually {listener.isExecuted && Person.count()} + + when: + datastore.currentSession.flush() + datastore.currentSession.clear() + danny = Person.get(danny.id) + + then: + danny.occupation + danny.occupation.endsWith("listener") + } + + static class PersonSaveOrUpdatePersistentEventListener extends AbstractPersistenceEventListener { + + boolean isExecuted + + protected PersonSaveOrUpdatePersistentEventListener(Datastore datastore) { + super(datastore) + } + + @Override + protected void onPersistenceEvent(AbstractPersistenceEvent event) { + if (event.entityObject instanceof Person) { + Person person = (Person) event.entityObject + person.occupation = person.occupation + " listener" + } + isExecuted = true + } + + @Override + boolean supportsEventType(Class eventType) { + return eventType == PreUpdateEvent || eventType == PreInsertEvent + } + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/PropertyFieldSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/PropertyFieldSpec.groovy new file mode 100644 index 00000000000..4d55b1d1ca5 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/PropertyFieldSpec.groovy @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.dirtychecking + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 05/05/2017. + */ +class PropertyFieldSpec extends Specification { + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(getClass().getPackage()) + + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/934') + void "test domain class with property named 'property'"() { + expect: + Book book = new Book(title: 'book', property: new Property(name: 'p1')) + book.save() + book.title == 'book' + } +} + +@Entity +class Property { + String name +} + +@Entity +class Book { + String title + Property property +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/events/UpdatePropertyInEventListenerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/events/UpdatePropertyInEventListenerSpec.groovy new file mode 100644 index 00000000000..a1815e10f55 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/events/UpdatePropertyInEventListenerSpec.groovy @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.events + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener +import org.grails.datastore.mapping.engine.event.PreInsertEvent +import org.grails.datastore.mapping.engine.event.PreUpdateEvent +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.Session +import org.hibernate.engine.spi.SessionImplementor +import org.springframework.context.ApplicationEvent +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 05/04/2017. + */ +class UpdatePropertyInEventListenerSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(User) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.transactionManager + + @Rollback + void "Test that using an listener does not produce an extra update"() { + given: + ((ConfigurableApplicationEventPublisher)hibernateDatastore.applicationEventPublisher).addApplicationListener( + new PasswordEncodingListener(hibernateDatastore) + ) + Session session = hibernateDatastore.sessionFactory.currentSession + + when:"A user is inserted" + User user = new User(username: "foo", password: "bar") + user.save(flush:true) + + then:"The password is only encoded once and no update is issued" + user.password == "xxxxxxxx0" + + when:"A user is found" + session.clear() + user = User.findByUsername("foo") + session.flush() + + then:"The password is not encoded again" + user.password == "xxxxxxxx0" + + when:"The user is updated" + user.password = "blah" + user.save(flush:true) + + then:"The password is encoded again" + user.password == "xxxxxxxx1" + + when:"A user is found" + session.clear() + user = User.findByUsername("foo") + session.flush() + + then:"The password is not encoded again" + user.password == "xxxxxxxx1" + } +} + +@Entity +class User { + String username + String password + + static mapping = { + table '`user`' + password column: '`password`' + } +} + +class PasswordEncodingListener extends AbstractPersistenceEventListener { + + int i = 0 + + PasswordEncodingListener(Datastore datastore) { + super(datastore) + } + + @Override + protected void onPersistenceEvent(AbstractPersistenceEvent event) { + event.getEntityAccess().setProperty("password", "xxxxxxxx${i++}".toString()) + } + + @Override + boolean supportsEventType(Class eventType) { + return eventType == PreUpdateEvent || eventType == PreInsertEvent + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/HasManyWithInQuerySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/HasManyWithInQuerySpec.groovy new file mode 100644 index 00000000000..8387d4cfed7 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/HasManyWithInQuerySpec.groovy @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.hasmany + +import grails.gorm.DetachedCriteria +import grails.gorm.annotation.Entity +import grails.gorm.services.Service +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +@Issue('https://github.com/grails/grails-data-hibernate5/issues/78') +@Rollback +class HasManyWithInQuerySpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(getClass().getPackage()) + + @Shared PublicationService publicationService = datastore.getService(PublicationService) + @Shared BookService bookService = datastore.getService(BookService) + + + @Ignore + void "test 'in' criteria"() { + setupData() + + when: + Book book = Book.get(1) + + then: + publicationService.findAllByBook(book) + } + + private Long setupData() { + Publication publication = new Publication(name: "OCI").save(flush: true, failOnError: true) + publication = addBooks(publication) + publication.id + } + + private List createBooks() { + List books = [] + ["Grails Goodness Notebook", + "Falando de Grails", + "The Definitive Guide to Grails 2", + "Grails 3 - Step by Step", + "Making Java Groovy", + "Grails in Action", "Practical Grails 3" + ].each { String title -> + books << bookService.save(title) + } + books + } + + private Publication addBooks(Publication publication) { + ["Grails Goodness Notebook", + "Falando de Grails", + "The Definitive Guide to Grails 2", + "Grails 3 - Step by Step", + "Making Java Groovy", + "Grails in Action", "Practical Grails 3" + ].each { String title -> + publicationService.addToBook(publication, title) + } + publication.save(flush: true) + } + +} + +@Entity +class Publication { + + String name + + static hasMany = [books: Book] +} + +@Entity +class Book { + + String title +} + +@Service +abstract class PublicationService { + List findAllByBook(Book book) { + def criteria = new DetachedCriteria(Publication).build { + inList("books", [book]) + } + criteria.list() + } + + Publication addToBook(Publication publication, String title) { + publication.addToBooks(new Book(title: title)) + } +} + +@Service(Book) +interface BookService { + + Book save(String title) + +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/ListCollectionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/ListCollectionSpec.groovy new file mode 100644 index 00000000000..dc864638a78 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/ListCollectionSpec.groovy @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.hasmany + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.mapping.collection.PersistentCollection +import org.grails.datastore.mapping.proxy.ProxyHandler +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Specification + +class ListCollectionSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(getClass().getPackage()) + + @Rollback + void "test legs are not loaded eagerly"() { + given: + new Animal(name: "Chloe") + .addToLegs(new Leg()) + .addToLegs(new Leg()) + .addToLegs(new Leg()) + .addToLegs(new Leg()) + .save(flush: true, failOnError: true) + datastore.currentSession.flush() + datastore.currentSession.clear() + ProxyHandler ph = datastore.mappingContext.proxyHandler + + when: + Animal animal = Animal.load(1) + animal = ph.unwrap(animal) + + then: + ph.isProxy(animal.legs) && !ph.isInitialized(animal.legs) + } +} + +@Entity +class Animal { + String name + + List legs + static hasMany = [legs: Leg] +} + +@Entity +class Leg { + +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/TwoUnidirectionalHasManySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/TwoUnidirectionalHasManySpec.groovy new file mode 100644 index 00000000000..f27bc46a1b3 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/TwoUnidirectionalHasManySpec.groovy @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.hasmany + +import grails.gorm.annotation.Entity +import grails.gorm.annotation.JpaEntity +import grails.gorm.hibernate.mapping.MappingBuilder +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +import jakarta.persistence.CascadeType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.OneToMany + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class TwoUnidirectionalHasManySpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(getClass().getPackage()) + + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/10811') + @Ignore + void "test two undirectional one to many references"() { + when: + new EcmMask(name: "test") + .addToCreateUsers(name: "Fred") + .addToUpdateUsers(name:"Bob") + .save(flush:true).discard() + + EcmMask mask = EcmMask.first() + + then: + mask != null + mask.createUsers.size() == 1 + mask.updateUsers.size() == 1 + + } + + @Rollback + @Issue('https://github.com/apache/grails-core/issues/10811') + @Ignore + void "test two JPA undirectional one to many references"() { + + when: + def jpa = new EcmMaskJpa(name: "test") + jpa.createdUsers.add(new User2(name: "Fred")) + jpa.updatedUsers.add(new User2(name: "Bob")) + + jpa.save(flush:true).discard() + + EcmMaskJpa mask = EcmMaskJpa.first() + + then: + mask != null + mask.createUsers.size() == 1 + mask.updateUsers.size() == 1 + + } + +} + +@JpaEntity +class EcmMaskJpa { + @Id + @GeneratedValue + Long id + + String name + + @OneToMany(cascade = CascadeType.ALL) + Set createdUsers = [] + + @OneToMany(cascade = CascadeType.ALL) + Set updatedUsers = [] +} + +@JpaEntity +class User2 { + @Id + @GeneratedValue + Long id + String name +} + +@Entity +class EcmMask { + String name + static hasMany = [createUsers:User,updateUsers:User] + + static mapping = MappingBuilder.orm { +// property('createUsers') { +// joinTable { name"created_users" } +// } +// property('updateUsers') { +// joinTable { name "updated_users" } +// } + } +} + +@Entity +class User { + String name + + static mapping = { + table '`user`' + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/SubclassToOneProxySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/SubclassToOneProxySpec.groovy new file mode 100644 index 00000000000..483d38dca61 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/SubclassToOneProxySpec.groovy @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.inheritance + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec + +class SubclassToOneProxySpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([SuperclassProxy, SubclassProxy, HasOneProxy]) + } + + void "the hasOne is a proxy and unwraps"() { + given: + SubclassProxy dog = new SubclassProxy().save() + new HasOneProxy(superclassProxy: dog).save() + manager.session.flush() + manager.session.clear() + HasOneProxy owner = HasOneProxy.first() + + expect: + manager.session.mappingContext.proxyFactory.isProxy(owner.@superclassProxy) + } +} + +@Entity +class SuperclassProxy { +} + +@Entity +class SubclassProxy extends SuperclassProxy { +} + +@Entity +class HasOneProxy { + SuperclassProxy superclassProxy +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/TablePerConcreteClassAndDateCreatedSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/TablePerConcreteClassAndDateCreatedSpec.groovy new file mode 100644 index 00000000000..407610fe964 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/TablePerConcreteClassAndDateCreatedSpec.groovy @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.inheritance + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import spock.lang.Issue + +/** + * Created by graemerocher on 29/05/2017. + */ +@Issue('https://github.com/apache/grails-data-mapping/issues/937') +class TablePerConcreteClassAndDateCreatedSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Vehicle, Spaceship]) + } + + void "should set the dateCreated automatically"() { + given: + Spaceship ship = new Spaceship(name: "Heart of Gold") + ship.save(flush: true) + + expect: + ship.dateCreated != null + } + + void "should set the dateCreated automatically on update"() { + given: + Spaceship ship = new Spaceship(name: "Heart of Gold") + ship.save() + + when: + ship.name = "Heart of Gold II" + ship.save(flush: true) + + then: + // DataIntegrityViolationException is thrown: + // NULL not allowed for column "DATE_CREATED" + ship.dateCreated != null + } +} + +@Entity +abstract class Vehicle { + String name + Date dateCreated + + static mapping = { + tablePerConcreteClass true + dynamicUpdate true + id generator: 'increment' + } +} + +@Entity +class Spaceship extends Vehicle { + static mapping = { + dynamicUpdate true + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/TablePerConcreteClassImportedSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/TablePerConcreteClassImportedSpec.groovy new file mode 100644 index 00000000000..4e68a044239 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/inheritance/TablePerConcreteClassImportedSpec.groovy @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.inheritance + +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import spock.lang.Issue + +@Issue('https://github.com/grails/grails-data-hibernate5/issues/151') +class TablePerConcreteClassImportedSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Vehicle, Spaceship]) + } + + void "test that subclasses are added to the imports on the metamodel"() { + expect: + manager.sessionFactory.getMetamodel().getImportedClassName('Vehicle') + manager.sessionFactory.getMetamodel().getImportedClassName('Spaceship') + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/jpa/SimpleJpaEntitySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/jpa/SimpleJpaEntitySpec.groovy new file mode 100644 index 00000000000..19a12df832f --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/jpa/SimpleJpaEntitySpec.groovy @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.jpa + +import grails.gorm.hibernate.HibernateEntity +import grails.gorm.transactions.Rollback +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.types.Association +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.OneToMany +import jakarta.validation.ConstraintViolationException +import jakarta.validation.constraints.Digits + +/** + * Created by graemerocher on 22/12/16. + */ +class SimpleJpaEntitySpec extends Specification { + + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(Customer) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.getTransactionManager() + + @Rollback + void "test that JPA entities can be treated as GORM entities"() { + when:"A basic entity is persisted and validated" + Customer c = new Customer(firstName: "6000.01", lastName: "Flintstone") + c.save(flush:true, validate:false) + + def query = Customer.where { + lastName == 'Rubble' + } + then:"The object was saved" + Customer.get(null) == null + Customer.get("null") == null + Customer.get(c.id) != null + !c.errors.hasErrors() + Customer.count() == 1 + query.count() == 0 + } + + @Rollback + void "test that JPA entities can use jakarta.validation"() { + when:"A basic entity is persisted and validated" + Customer c = new Customer(firstName: "Bad", lastName: "Flintstone") + c.save(flush:true) + + def query = Customer.where { + lastName == 'Rubble' + } + then:"The object was saved" + c.errors.hasErrors() + Customer.count() == 0 + query.count() == 0 + } + + @Rollback + void "test that JPA entities can use jakarta.validation and the hibernate interceptor evicts invalid entities"() { + when:"A basic entity is persisted and validated" + Customer c = new Customer(firstName: "Bad", lastName: "Flintstone") + c.save(flush:true, validate:false) + + def query = Customer.where { + lastName == 'Rubble' + } + then:"The object was saved" + thrown(ConstraintViolationException) + c.errors.hasErrors() + } + + void "Test persistent entity model"() { + given: + PersistentEntity entity = hibernateDatastore.mappingContext.getPersistentEntity(Customer.name) + + expect: + entity.identity.name == 'myId' + entity.associations.size() == 1 + entity.associations.find { Association a -> a.name == 'related' } + } +} + +@Entity +class Customer implements HibernateEntity { + @Id + @GeneratedValue + Long myId + @Digits(integer = 6, fraction = 2) + String firstName + String lastName + + @OneToMany + Set related +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/mappedby/MultipleOneToOneSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/mappedby/MultipleOneToOneSpec.groovy new file mode 100644 index 00000000000..b502cb9f417 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/mappedby/MultipleOneToOneSpec.groovy @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.mappedby + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import spock.lang.Issue + +/** + * Created by graemerocher on 29/05/2017. + */ +class MultipleOneToOneSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Org, OrgMember]) + } + + @Issue('https://github.com/apache/grails-data-mapping/issues/950') + void "test mappedBy with multiple many-to-one and a single one-to-one"() { + given: + Org branch = new Org(id: 1, name: "branch a").save() + new OrgMember(org: branch).save(flush: true) + def query = OrgMember.where({ branch == null }) + + expect: + query.updateAll(branch: branch) == 1 + OrgMember.findByBranch(branch) + } +} + + +@Entity +class Org { + + String name + + OrgMember member + + static mappedBy = [member: "org"] + + static constraints = { + member nullable: true + } + + static mapping = { + id generator: "assigned" + } + +} + +@Entity +class OrgMember { + static belongsTo = [org: Org] + + Org branch + Org division + Org region + + static mappedBy = [branch: "none", division: "none", region: "none"] + + static constraints = { + org nullable: false + branch nullable: true + division nullable: true + region nullable: true + } + +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyBidirectionalManyToManySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyBidirectionalManyToManySpec.groovy new file mode 100644 index 00000000000..c36b25199b5 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyBidirectionalManyToManySpec.groovy @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.multitenancy + +import grails.gorm.MultiTenant +import grails.gorm.annotation.Entity +import grails.gorm.multitenancy.CurrentTenant +import grails.gorm.services.Service +import grails.gorm.transactions.Rollback +import grails.gorm.transactions.Transactional +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings +import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.dialect.H2Dialect +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Issue +import spock.util.environment.RestoreSystemProperties +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by puneetbehl on 21/03/2018. + */ +@RestoreSystemProperties +class MultiTenancyBidirectionalManyToManySpec extends Specification { + + final Map config = [ + "grails.gorm.multiTenancy.mode":MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR, + "grails.gorm.multiTenancy.tenantResolverClass":SystemPropertyTenantResolver.name, + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create', + ] + + @Shared DepartmentService departmentService + @Shared UserService userService + + @Shared @AutoCleanup HibernateDatastore datastore + + + void setup() { + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "oci") + datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), getClass().getPackage() ) + departmentService = datastore.getService(DepartmentService) + userService = datastore.getService(UserService) + } + + @Rollback + @Issue("https://github.com/grails/grails-data-hibernate5/issues/58") + void "test hasMany and 'in' query with multi-tenancy" () { + given: + createSomeUsers() + + when: + List users = userService.findAllByDepartment("Grails") + + then: + users.size() == 4 + } + + Number createSomeUsers() { + Department department = departmentService.save("Grails") + department.addToUsers(username: "John Doe").save() + department.addToUsers(username: "Hanna William").save() + department.addToUsers(username: "Mark").save() + department.addToUsers(username: "Karl").save() + department.save(flush: true) + department.users.size() + } + +} + +@Entity +class User implements MultiTenant { + String username + String tenantId + + static belongsTo = [Department] + static hasMany = [departments: Department] + + static mapping = { + table '`user`' + } +} + +@Entity +class Department implements MultiTenant { + String name + String tenantId + + static hasMany = [users: User] +} + +@CurrentTenant +@Service(Department) +@Transactional +abstract class DepartmentService { + + UserService userService + + abstract Department save(String name) + + abstract Department save(Department department) + + List findAllByUser(String username) { + User user = User.findByUsername(username) + Department.executeQuery('from Department d where :user in elements(d.users)', [user: user]) + } + + abstract Number count() + +} + +@CurrentTenant +@Service(User) +@Transactional +abstract class UserService { + + List findAllByDepartment(String departmentName) { + Department department = Department.findByName(departmentName) + User.executeQuery('from User u where :department in elements(u.departments)', [department: department]) + } + + abstract User save(User user) + + abstract Number count() +} + + + + diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyUnidirectionalOneToManySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyUnidirectionalOneToManySpec.groovy new file mode 100644 index 00000000000..72d5faf0b60 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyUnidirectionalOneToManySpec.groovy @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.multitenancy + +import grails.gorm.annotation.Entity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings +import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver +import grails.gorm.MultiTenant +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.dialect.H2Dialect +import spock.lang.Issue +import spock.lang.Specification + +/** + * Created by graemerocher on 16/06/2017. + */ +class MultiTenancyUnidirectionalOneToManySpec extends Specification { + + @Issue('https://github.com/apache/grails-data-mapping/issues/954') + void "test multi-tenancy with unidirectional one-to-many"() { + given: "A configuration for schema based multi-tenancy" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "") + Map config = [ + "grails.gorm.multiTenancy.mode" : MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR, + "grails.gorm.multiTenancy.tenantResolverClass": SystemPropertyTenantResolver.name, + 'dataSource.url' : "jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dialect' : H2Dialect.name, + 'dataSource.formatSql' : 'true', + 'hibernate.flush.mode' : 'COMMIT', + 'hibernate.cache.queries' : 'true', + 'hibernate.hbm2ddl.auto' : 'create', + ] + + HibernateDatastore datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), getClass().getPackage()) + + when: + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "ford") + Vehicle.withTransaction { + new Vehicle(model: "A5", year: 2017, manufacturer: "Audi") + .addToEngines(cylinders: 6, manufacturer: "VW") + .addToWheels(spokes: 5) + .save(flush: true) + } + + then: + Vehicle.withTransaction { Vehicle.count() } == 1 + Vehicle.withTransaction { + Vehicle.first().engines.size() + } == 1 + Vehicle.withTransaction { + Vehicle.where { year == 2017 }.list(fetch: [engines: "join", wheels: "join"]).size() + } == 1 + + when: + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "tesla") + + then: + Vehicle.withTransaction { Vehicle.count() } == 0 + + cleanup: + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "") + } +} + + +@Entity +class Engine implements MultiTenant { + Integer cylinders + String manufacturer +// static belongsTo = [vehicle: Vehicle] // If you remove this, it fails + + static constraints = { + cylinders nullable: false + } + + static mapping = { + tenantId name: 'manufacturer' + } +} + +@Entity +class Wheel implements MultiTenant { + Integer spokes + String manufacturer +// static belongsTo = [vehicle: Vehicle] // If you remove this, it fails + + static constraints = { + spokes nullable: false + } + + static mapping = { + tenantId name: 'manufacturer' + } +} + +@Entity +class Vehicle implements MultiTenant { + String model + Integer year + String manufacturer + + static hasMany = [engines: Engine, wheels: Wheel] + static constraints = { + model blank: false + year min: 1980 + } + + static mapping = { + tenantId name: 'manufacturer' + year column: 'vehicleYear' + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/perf/JoinPerfSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/perf/JoinPerfSpec.groovy new file mode 100644 index 00000000000..252d9a09fab --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/perf/JoinPerfSpec.groovy @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.perf + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import groovy.sql.Sql +import groovy.transform.EqualsAndHashCode +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import jakarta.persistence.AccessType + +/** + * Created by graemerocher on 08/12/16. + */ +@Rollback +class JoinPerfSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(Author, Book, BookAuthor) + @Shared PlatformTransactionManager transactionManager = datastore.getTransactionManager() + + void setup() { + for(i in 0..500) { + Author a = new Author(name: "Author $i").save() + + for(j in 0..3) { + new Book(title: "Book $i - $j").save() + } + datastore.sessionFactory.currentSession.flush() + datastore.sessionFactory.currentSession.clear() + } + + for(i in 0..7000) { + Author a = Author.get(Math.abs(new Random().nextInt() % 500) + 1) + Book b = Book.get(Math.abs(new Random().nextInt() % 1500) + 1) + if(a && b) { + new BookAuthor(book: b, author: a).save() + } + datastore.sessionFactory.currentSession.flush() + datastore.sessionFactory.currentSession.clear() + } + } + + void 'test read performance with join query'() { + when: + def authors = Author.findAll().groupBy { it.id } + def books = Book.findAll().groupBy { it.id } + datastore.sessionFactory.currentSession.clear() + long time = System.nanoTime(); + + BookAuthor.findAll().size() + long domainsLoadedAt = System.nanoTime() + long timeOfDomainClassLoad = domainsLoadedAt - time; + + int itemsLoaded = 0 + new Sql(datastore.connectionSources.defaultConnectionSource.dataSource).eachRow("select author_id, book_id from book_author") { row -> + assert authors.get(row.author_id) + assert books.get(row.book_id) + itemsLoaded++ + } + long timeOfPlainQuery = System.nanoTime() - domainsLoadedAt; + + println "Loaded BookAuthor domains in ${timeOfDomainClassLoad / 1000000.0}ms while query took ${timeOfPlainQuery / 1000000.0}ms" + + then:"the assertion here doesn't matter much, we're testing perf not logic" + BookAuthor.count() > 6000 + } +} + +@Entity +class Author { + String name +} +@Entity +class Book { + String title +} + +@Entity +@EqualsAndHashCode(includes = ['book', 'author']) +class BookAuthor implements Serializable{ + Book book + Author author + + static mapping = { + id composite:['book', 'author'] + version false + book accessType: AccessType.FIELD + author accessType: AccessType.FIELD + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/proxy/ByteBuddyProxySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/proxy/ByteBuddyProxySpec.groovy new file mode 100644 index 00000000000..8884c2782af --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/proxy/ByteBuddyProxySpec.groovy @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.proxy + +import grails.gorm.tests.Club +import grails.gorm.tests.Team +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.grails.datastore.mapping.reflect.ClassUtils +import org.grails.orm.hibernate.proxy.HibernateProxyHandler +import spock.lang.PendingFeatureIf +import spock.lang.Shared + +/** + * Contains misc proxy tests using Hibernate defaults, which is ByteBuddy. + * These should all be passing for Gorm to be operating correctly with Groovy. + */ +class ByteBuddyProxySpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([Team, Club]) + } + + @Shared + HibernateProxyHandler proxyHandler = new HibernateProxyHandler() + + //to show test that fail that should succeed set this to true. or uncomment the + // testImplementation "org.yakworks:hibernate-groovy-proxy:$yakworksHibernateGroovyProxy" to see pass + boolean runPending = ClassUtils.isPresent("yakworks.hibernate.proxy.ByteBuddyGroovyInterceptor") + + Team createATeam(){ + Club c = new Club(name: "DOOM Club").save(failOnError:true) + Team team = new Team(name: "The A-Team", club: c).save(failOnError:true, flush:true) + return team + } + + void "getId and id property checks dont initialize proxy if in a CompileStatic method"() { + when: + Team team = createATeam() + manager.session.clear() + team = Team.load(team.id) + + then:"The asserts on getId and id should not initialize proxy when statically compiled" + StaticTestUtil.team_id_asserts(team) + !proxyHandler.isInitialized(team) + + StaticTestUtil.club_id_asserts(team) + !proxyHandler.isInitialized(team.club) + } + + @PendingFeatureIf({ !instance.runPending }) + void "getId and id dont initialize proxy"() { + when:"load proxy" + Team team = createATeam() + manager.session.clear() + team = Team.load(team.id) + + then:"The asserts on getId and id should not initialize proxy" + proxyHandler.isProxy(team) + team.getId() + !proxyHandler.isInitialized(team) + + team.id + !proxyHandler.isInitialized(team) + + and: "the getAt check for id should not initialize" + team['id'] + !proxyHandler.isInitialized(team) + } + + @PendingFeatureIf({ !instance.runPending }) + void "truthy check on instance should not initialize proxy"() { + when:"load proxy" + Team team = createATeam() + manager.session.clear() + team = Team.load(team.id) + + then:"The asserts on the intance should not init proxy" + team + !proxyHandler.isInitialized(team) + + and: "truthy check on association should not initialize" + team.club + !proxyHandler.isInitialized(team.club) + } + + @PendingFeatureIf({ !instance.runPending }) + void "id checks on association should not initialize its proxy"() { + when:"load instance" + Team team = createATeam() + manager.session.clear() + team = Team.load(team.id) + + then:"The asserts on the intance should not init proxy" + !proxyHandler.isInitialized(team.club) + + team.club.getId() + !proxyHandler.isInitialized(team.club) + + team.club.id + !proxyHandler.isInitialized(team.club) + + team.clubId + !proxyHandler.isInitialized(team.club) + + and: "the getAt check for id should not initialize" + team.club['id'] + !proxyHandler.isInitialized(team.club) + } + + void "isDirty should not intialize the association proxy"() { + when:"load instance" + Team team = createATeam() + manager.session.clear() + team = Team.load(team.id) + + then:"The asserts on the intance should not init proxy" + !proxyHandler.isInitialized(team) + + //isDirty will init the proxy. should make changes for this. + !team.isDirty() + proxyHandler.isInitialized(team) + //it should not have initialized the association + !proxyHandler.isInitialized(team.club) + + when: "its made dirty" + team.name = "B-Team" + + then: + team.isDirty() + //still should not have initialized it. + !proxyHandler.isInitialized(team.club) + } + +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/proxy/StaticTestUtil.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/proxy/StaticTestUtil.groovy new file mode 100644 index 00000000000..3323f5f1666 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/proxy/StaticTestUtil.groovy @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.proxy + +import groovy.transform.CompileStatic + +import org.grails.datastore.gorm.GormEntity +import org.grails.orm.hibernate.proxy.HibernateProxyHandler +import org.hibernate.Hibernate + +import grails.gorm.tests.Club +import grails.gorm.tests.Team + +@CompileStatic +class StaticTestUtil { + public static HibernateProxyHandler proxyHandler = new HibernateProxyHandler() + + // should return true and not initialize the proxy + // getId works inside a compile static + static boolean team_id_asserts(Team team){ + assert team.getId() + assert !Hibernate.isInitialized(team) + assert proxyHandler.isProxy(team) + + assert team.id + assert !Hibernate.isInitialized(team) + assert proxyHandler.isProxy(team) + //a truthy check on the object will try to init it because it hits the getMetaClass + // assert team + // assert !Hibernate.isInitialized(team) + + return true + } + + static boolean club_id_asserts(Team team){ + assert team.club.getId() + assert notInitialized(team.club) + + assert team.club.id + assert notInitialized(team.club) + + assert team.clubId + assert notInitialized(team.club) + + return true + } + + static boolean notInitialized(Object o){ + //sanity check the 3 + assert !Hibernate.isInitialized(o) + assert !proxyHandler.isInitialized(o) + assert proxyHandler.isProxy(o) + return true + } +} + diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/services/DataServiceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/services/DataServiceSpec.groovy new file mode 100644 index 00000000000..a6024f83186 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/services/DataServiceSpec.groovy @@ -0,0 +1,536 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.services + +import grails.gorm.annotation.Entity +import grails.gorm.services.Join +import grails.gorm.services.Query +import grails.gorm.services.Service +import grails.gorm.services.Where +import grails.gorm.transactions.Rollback +import grails.gorm.validation.PersistentEntityValidator +import grails.validation.ValidationException +import groovy.json.DefaultJsonGenerator +import groovy.json.JsonGenerator +import org.grails.datastore.gorm.validation.constraints.eval.DefaultConstraintEvaluator +import org.grails.datastore.gorm.validation.constraints.registry.DefaultConstraintRegistry +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.context.support.StaticMessageSource +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 07/04/2017. + */ +@Rollback +class DataServiceSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(getClass().getPackage()) + + + void "test inter service interaction"() { + given: + Product p1 = new Product(name: "Apple", type:"Fruit").save(flush:true) + Product p2 = new Product(name: "Orange", type:"Fruit").save(flush:true) + AnotherProductService productService = datastore.getService(AnotherProductService) + + expect: + productService.findProductInfo("Apple", "Fruit").name == "Apple" + + } + + void "test list products"() { + given: + Product p1 = new Product(name: "Apple", type:"Fruit").save(flush:true) + Product p2 = new Product(name: "Orange", type:"Fruit").save(flush:true) + ProductService productService = datastore.getService(ProductService) + + expect: + productService.listWithArgs(max:1).size() == 1 + productService.listProducts().size() == 2 + productService.listMoreProducts().length == 2 + productService.findEvenMoreProducts().iterator().hasNext() + productService.findByName("Apple").iterator().hasNext() + productService.findProducts("Apple", "Fruit").iterator().hasNext() + !productService.findProducts("Apple", "Devices").iterator().hasNext() + !productService.findByName("Banana").iterator().hasNext() + productService.findProducts("Apple").iterator().hasNext() + !productService.findProducts("Banana").iterator().hasNext() + productService.getByName("Apple") != null + productService.getByName("Apple").name == "Apple" + productService.getByName("Banana") == null + p1.name == productService.get(p1.id)?.name + productService.get(100) == null + productService.find("Apple", "Fruit") != null + productService.find("Orange", "Fruit").name == "Orange" + productService.find("Apple", "Fruit", [max:2]) != null + productService.find("Apple", "Device") == null + } + + void "test delete by id implementation"() { + given: + Product p1 = new Product(name: "Apple", type:"Fruit").save(flush:true) + Product p2 = new Product(name: "Orange", type:"Fruit").save(flush:true) + ProductService productService = datastore.getService(ProductService) + + when: + Product found = productService.get(p1.id) + + then: + found != null + + when: + Product deleted = productService.deleteProduct(found.id) + + then: + deleted != null + productService.get(found.id) == null + + } + + void "test delete by parameter query implementation"() { + given: + Product p1 = new Product(name: "Apple", type:"Fruit").save(flush:true) + Product p2 = new Product(name: "Orange", type:"Fruit").save(flush:true) + ProductService productService = datastore.getService(ProductService) + + when: + Product found = productService.get(p1.id) + + then: + found != null + + when: + Product deleted = productService.delete("Apple") + datastore.sessionFactory.currentSession.flush() + + then: + deleted != null + productService.getByName(deleted.name) == null + + } + + void "test delete all implementation"() { + given: + Product p1 = new Product(name: "Apple", type:"Fruit").save(flush:true) + Product p2 = new Product(name: "Orange", type:"Fruit").save(flush:true) + ProductService productService = datastore.getService(ProductService) + + when: + Product found = productService.get(p1.id) + + then: + found != null + + when: + datastore.sessionFactory.currentSession.clear() + Number deleted = productService.deleteProducts("Apple") + + then: + deleted == 1 + productService.get(p1.id) == null + + } + + void "test delete with void return type"() { + given: + Product p1 = new Product(name: "Apple", type:"Fruit").save(flush:true) + Product p2 = new Product(name: "Orange", type:"Fruit").save(flush:true) + ProductService productService = datastore.getService(ProductService) + + when: + Product found = productService.get(p1.id) + + then: + found != null + + when: + Number deleted = productService.remove(p1.id) + + then: + deleted == 1 + productService.get(p1.id) == null + } + + void "test save entity"() { + given: + ProductService productService = datastore.getService(ProductService) + + when: + productService.saveProduct("Pineapple", "Fruit") + + then: + productService.find("Pineapple", "Fruit") != null + } + + void "test save invalid entity"() { + given: + def mappingContext = datastore.mappingContext + def entity = mappingContext.getPersistentEntity(Product.name) + def messageSource = new StaticMessageSource() + def evaluator = new DefaultConstraintEvaluator(new DefaultConstraintRegistry(messageSource), mappingContext, Collections.emptyMap()) + mappingContext.addEntityValidator( + entity, + new PersistentEntityValidator(entity, messageSource, evaluator) + ) + ProductService productService = datastore.getService(ProductService) + + when: + productService.saveProduct("", "Fruit") + + then: + thrown(ValidationException) + } + + void "test abstract class service impl"() { + given: + AnotherProductService productService = (AnotherProductService)datastore.getService(AnotherProductInterface) + + when: + Product p = productService.saveProduct("Apple", "Fruit") + + then: + datastore.getService(AnotherProductService) != null + p.id != null + productService.get(p.id) != null + + when: + productService.delete(p.id) + + then: + productService.get(p.id) == null + productService.getByName("blah").name == "BLAH" + + } + + void "test update one method"() { + given: + ProductService productService = datastore.getService(ProductService) + + when: + productService.saveProduct("Tomato", "Vegetable") + + then: + productService.find("Tomato", "Vegetable") != null + + when: + Product product = productService.find("Tomato", "Vegetable") + productService.updateProduct(product.id, "Fruit") + datastore.currentSession.flush() + + then: + productService.find("Tomato", "Vegetable") == null + productService.find("Tomato", "Fruit") != null + + } + + void 'test property projection'() { + given: + ProductService productService = datastore.getService(ProductService) + + when: + Product p = productService.saveProduct("Tomato", "Vegetable") + + then: + productService.findProductType(p.id) == "Vegetable" + } + + void 'test property projection return all types'() { + given: + ProductService productService = datastore.getService(ProductService) + + when: + productService.saveProduct("Carrot", "Vegetable") + productService.saveProduct("Pumpkin", "Vegetable") + productService.saveProduct("Tomato", "Fruit") + + then: + productService.listProductName("Vegetable").size() == 2 + productService.countProducts("Vegetable") == 2 + productService.countPrimProducts("Vegetable") == 2 + productService.countByType("Vegetable") == 2 + } + + void "test @where annotation"() { + given: + ProductService productService = datastore.getService(ProductService) + + when: + productService.saveProduct("Carrot", "Vegetable") + productService.saveProduct("Pumpkin", "Vegetable") + productService.saveProduct("Tomato", "Fruit") + + Product p = productService.searchByType("Veg%") + + then: + p != null + p.name == 'Carrot' + productService.searchByType("Stuf%") == null + productService.searchProducts("Veg%").size() == 2 + productService.howManyProducts("Veg%") == 2 + + } + + void "test @query annotation"() { + given: + ProductService productService = datastore.getService(ProductService) + + productService.saveProduct("Carrot", "Vegetable") + productService.saveProduct("Pumpkin", "Vegetable") + productService.saveProduct("Tomato", "Fruit") + + + when: + Product product = productService.searchWithQuery("Carr%") + + then: + product != null + product.name == "Carrot" + productService.searchProductType("Carr%") == "Vegetable" + + when: + List results = productService.searchAllWithQuery("Veg%") + + then: + results.size() == 2 + + when: + List names = productService.searchProductNames("Ve%") + + then: + names.size() == 2 + names == ["Carrot", "Pumpkin"] + + } + + void "test interface projection"() { + given: + ProductService productService = datastore.getService(ProductService) + + when: + productService.saveProduct("Carrot", "Vegetable") + productService.saveProduct("Pumpkin", "Vegetable") + productService.saveProduct("Tomato", "Fruit") + + ProductInfo info = productService.findProductInfo("Pumpkin", "Vegetable") + List infos = productService.findProductInfos( "Vegetable") + def result = new DefaultJsonGenerator(new JsonGenerator.Options().excludeFieldsByName("\$target")).toJson(info) + then: + infos.size() == 2 + infos.first().name == "Carrot" + result == '{"name":"Pumpkin"}' + info != null + info.name == "Pumpkin" + productService.searchProductInfoByName("Pump%") != null + productService.findByTypeLike("Veg%") != null + productService.findByTypeLike("Jun%") == null + productService.findAllByTypeLike( "Vege%").size() == 2 + + when: + info = productService.searchProductInfo("Pum%") + + then: + info.name == "Pumpkin" + + when: + datastore.sessionFactory.currentSession.clear() + productService.deleteSomeProducts("Vegetable") + + + then: + productService.findByTypeLike("Vege%") == null + + + } + + void "test join query on attributes with @Query"() { + given: + ProductService productService = datastore.getService(ProductService) + new Product(name: "Apple", type: "Fruit") + .addToAttributes(name: "round") + .save(flush:true) + new Product(name: "Banana", type: "Fruit") + .addToAttributes(name: "curved") + .save(flush:true) + + when: + List products = productService.findProductsWithAttributes("round") + + then: + products.size() == 1 + products[0].name == "Apple" + } + + @Issue('https://github.com/apache/grails-data-mapping/issues/960') + void "test findBy dynamic finder with @Join doesn't return proxies"() { + given: + ProductService productService = datastore.getService(ProductService) + new Product(name: "Apple", type: "Fruit") + .addToAttributes(name: "round") + .save(flush:true) + + new Product(name: "Banana", type: "Fruit") + .addToAttributes(name: "curved") + .save(flush:true) + + datastore.currentSession.clear() + + when: + Product product = productService.findByName("Apple").first() + + then: + product.attributes.isInitialized() + } +} + +interface ProductInfo { + String getName() +} + +@Entity +class Product { + String name + String type + + static hasMany = [attributes:Attribute] + + static constraints = { + name blank:false + } +} + +@Entity +class Attribute { + String name +} + +interface AnotherProductInterface { + Product saveProduct(String name, String type) + + Number delete(Serializable id) +} + +@Service(Product) +abstract class AnotherProductService implements AnotherProductInterface{ + + ProductService originalProductService + + abstract Product get(Serializable id) + + Product getByName(String name) { + return new Product(name:name.toUpperCase()) + } + + ProductInfo findProductInfo(String name, String type) { + getOriginalProductService().findProductInfo(name, type) // ? + } +} + +@Service(Product) +interface ProductService { + List findProductInfos(String type) + + List findAllByTypeLike(String type) + + ProductInfo findProductInfo(String name, String type) + + @Query(""" + SELECT $p FROM ${Product p} JOIN ${Attribute attr = p.attributes} WHERE ${attr.name} = $name + """) + List findProductsWithAttributes(String name) + + @Query("from ${Product p} where $p.name like $pattern") + ProductInfo searchProductInfo(String pattern) + + ProductInfo findByTypeLike(String type) + + @Where({ name ==~ pattern }) + ProductInfo searchProductInfoByName(String pattern) + + @Query("from ${Product p} where $p.name like $pattern") + Product searchWithQuery(String pattern) + + @Query("select ${p.type} from ${Product p} where $p.name like $pattern") + String searchProductType(String pattern) + + @Query("from ${Product p} where $p.type like $pattern") + List searchAllWithQuery(String pattern) + + @Query("select $p.name from ${Product p} where $p.type like $pattern") + List searchProductNames(String pattern) + + @Where({ type ==~ pattern }) + Product searchByType(String pattern) + + @Where({ type ==~ pattern }) + Set searchProducts(String pattern) + + @Where({ type ==~ pattern }) + Number howManyProducts(String pattern) + + List listProductName(String type) + + String findProductType(Serializable id) + + Number countByType(String t) + + Number countProducts(String type) + + int countPrimProducts(String type) + + Product updateProduct(Serializable id, String type) + + Product saveProduct(String name, String type) + + Number deleteProducts(String name) + + @Where({ type == type }) + Number deleteSomeProducts(String type) + Product delete(String name) + + Number remove(Serializable id) + + Product deleteProduct(Serializable id) + + Product get(Serializable id) + + Product getByName(String name) + + Product find(String name, String type) + + Product find(String name, String type, Map args) + + @Join('attributes') + List findProducts(String name) + + List findProducts(String name, String type) + + List listWithArgs(Map args) + + List listProducts() + + Product[] listMoreProducts() + + Iterable findEvenMoreProducts() + + @Join('attributes') + Iterable findByName(String n) +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/softdelete/SoftDeleteSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/softdelete/SoftDeleteSpec.groovy new file mode 100644 index 00000000000..244e07d8fdd --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/softdelete/SoftDeleteSpec.groovy @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.softdelete + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.gorm.GormEntity +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class SoftDeleteSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(getClass().getPackage()) + + + @Rollback + void 'test soft delete'() { + given: + new Person(name: "Fred").save(flush:true) + + when: + Person p = Person.first() + + then: + !p.deleted + + when: + p.delete(flush:true) + p.discard() + + p = Person.first() + then: + p.deleted + + } +} + +@Entity +class Person implements SoftDelete { + String name +} + +trait SoftDelete extends GormEntity { + boolean deleted = false + @Override + void delete() { + markDirty('deleted', false, true) + deleted = true + save() + } + + @Override + void delete(Map params) { + markDirty('deleted', false, true) + deleted = true + save(params) + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/traits/InterfacePropertySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/traits/InterfacePropertySpec.groovy new file mode 100644 index 00000000000..89c499945c0 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/traits/InterfacePropertySpec.groovy @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.traits + +import grails.gorm.annotation.Entity +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import spock.lang.Issue + +/** + * Created by graemerocher on 29/05/2017. + */ +class InterfacePropertySpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([TestDomain]) + } + + @Issue('https://github.com/grails/grails-data-hibernate5/issues/38') + void "test interface that exposes id"() { + when: + TestDomain td = new TestDomain(name: "Fred").save(flush: true) + + then: + td.id + TestDomain.first().id + } +} + +interface ObjectId extends Serializable { + T getId() + + void setId(T id) +} + +@Entity +class TestDomain implements ObjectId { + + Long id + String name + + static constraints = { + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/traits/TraitPropertySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/traits/TraitPropertySpec.groovy new file mode 100644 index 00000000000..685721f021a --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/traits/TraitPropertySpec.groovy @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.traits + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 02/05/2017. + */ +class TraitPropertySpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(getClass().getPackage()) + + @Rollback + void "test entity with trait property"() { + when: + new EntityWithTrait(name: "test", bar: "test2").save(flush:true) + EntityWithTrait obj = EntityWithTrait.first() + + then: + obj.name == "test" + obj.bar == "test2" + } +} + +trait Foo { + String bar +} + +@Entity +class EntityWithTrait implements Foo { + String name +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/CustomIsolationLevelSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/CustomIsolationLevelSpec.groovy new file mode 100644 index 00000000000..6e74e89b98e --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/CustomIsolationLevelSpec.groovy @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.txs + +import grails.gorm.tests.services.Attribute +import grails.gorm.tests.services.Product +import grails.gorm.transactions.Transactional +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.annotation.Isolation +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 16/06/2017. + */ +class CustomIsolationLevelSpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore hibernateDatastore = new HibernateDatastore(Product, Attribute) + + + @Issue('https://github.com/apache/grails-data-mapping/issues/952') + void "test custom isolation level"() { + expect: + new ProductService().listProducts().size() == 0 + } + + +} + +class ProductService { + @Transactional(isolation = Isolation.SERIALIZABLE) + List listProducts() { + Product.list() + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/TransactionPropagationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/TransactionPropagationSpec.groovy new file mode 100644 index 00000000000..d29e0016874 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/TransactionPropagationSpec.groovy @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.txs + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.ReadOnly +import grails.gorm.transactions.Transactional +import org.grails.orm.hibernate.HibernateDatastore +import org.springframework.transaction.annotation.Propagation +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class TransactionPropagationSpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore hibernateDatastore = new HibernateDatastore(Book) + + @Issue('https://github.com/apache/grails-core/issues/10801') + void "test transaction propagation settings"() { + when: + TransactionalService service = new TransactionalService() + service.start() + + then: + def e = thrown(RuntimeException) + e.message == 'foo' + service.count() == 1 + service.first().name == 'two' + } + +} + +@Transactional +class TransactionalService { + + @ReadOnly + int count() { + Book.count + } + + @ReadOnly + Book first() { + Book.first() + } + + def start() { + createBook() + createAnotherBook() + throw new RuntimeException('foo') + } + + def createBook() { + new Book(name: 'one').save(failOnError: true) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + def createAnotherBook() { + new Book(name: 'two').save(failOnError: true) + } + +} +@Entity +class Book { + + String name + + static constraints = { + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/TransactionalWithinReadOnlySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/TransactionalWithinReadOnlySpec.groovy new file mode 100644 index 00000000000..82ac8c3a402 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/txs/TransactionalWithinReadOnlySpec.groovy @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.txs + +import grails.gorm.tests.services.Attribute +import grails.gorm.tests.services.Product +import grails.gorm.transactions.ReadOnly +import grails.gorm.transactions.Transactional +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class TransactionalWithinReadOnlySpec extends Specification { + + @Shared + @AutoCleanup + HibernateDatastore datastore = new HibernateDatastore(Product, Attribute) + + void "test transaction status"() { + given: + TxService txService = new TxService() + + expect: + txService.readProduct() + !txService.writeProduct() + } +} + +@ReadOnly +class TxService { + + boolean readProduct() { + def tx = transactionStatus + tx.readOnly + } + + @Transactional + boolean writeProduct() { + def tx = transactionStatus + tx.readOnly + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/uuid/UuidInsertSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/uuid/UuidInsertSpec.groovy new file mode 100644 index 00000000000..fb1f76dafb3 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/uuid/UuidInsertSpec.groovy @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.uuid + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 05/04/2017. + */ +class UuidInsertSpec extends Specification { + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(getClass().getPackage()) + + + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/902') + void "Test UUID insert"() { + when:"A UUID is used" + Person p = new Person(name: "test").save(flush:true) + + then:"An update should not be triggered" + p.id + p.name == 'test' + } +} + +@Entity +class Person { + UUID id + String name + + def beforeUpdate() { + name = "changed" + } + static mapping = { + id generator : 'uuid2', type: 'uuid-binary' + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/BeanValidationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/BeanValidationSpec.groovy new file mode 100644 index 00000000000..b844a2f8a9f --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/BeanValidationSpec.groovy @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests.validation + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import jakarta.validation.constraints.Digits +import jakarta.validation.constraints.NotBlank + +/** + * Created by graemerocher on 07/04/2017. + */ +class BeanValidationSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(Bean) + + @Rollback + void "test bean validation API validate on save"() { + given:"A an invalid instance" + Bean bean = new Bean(name:"", price:600.12034) + when:"the bean is saved" + bean.save() + + then:"the errors are correct" + bean.hasErrors() + bean.errors.allErrors.size() == 2 + bean.errors.hasFieldErrors("price") + bean.errors.hasFieldErrors("name") + } +} + +@Entity +class Bean { + @NotBlank + String name + @Digits(integer = 6, fraction = 2) + Double price +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/CascadeValidationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/CascadeValidationSpec.groovy new file mode 100644 index 00000000000..2ed99ac94a9 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/CascadeValidationSpec.groovy @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.tests.validation + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 04/05/2017. + */ +class CascadeValidationSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(Business, Person, Employee) + + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/926') + void "validation cascades correctly"() { + given: "an invalid business" + Business b = new Business(name: null) + + and: "a valid employee that belongs to the business" + Person p = new Employee(business: b) + b.addToPeople(p) + + when: + b.save() + + then: + b.errors.hasFieldErrors('name') + b.hasErrors() + } +} +@Entity +class Business { + + String name + + static hasMany = [ + people: Person + ] + +} +@Entity +abstract class Person { + +} + +@Entity +class Employee extends Person { + + static belongsTo = [ + business: Business + ] + +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/DeepValidationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/DeepValidationSpec.groovy new file mode 100644 index 00000000000..43753bc952b --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/DeepValidationSpec.groovy @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.validation + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import org.springframework.dao.DataIntegrityViolationException +import spock.lang.Issue + +/** + * Created by francoiskha on 19/04/18. + */ +class DeepValidationSpec extends GrailsDataTckSpec { + void setupSpec() { + manager.domainClasses.addAll([AnotherCity, Market, Address]) + } + + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/1033') + void "performs deep validation correctly"() { + + when: "save market with failing custom validator on child" + Address address = new Address(streetName: "Main St.", landmark: "The Golder Gate Bridge", postalCode: "11").save(validate: false) + new Market(name: "Main", address: address).save(deepValidate: false) + + then: "market is saved, no validation error" + Market.count() == 1 + + when: "save market with nullable on child" + address = new Address(landmark: "1B, Main St.", postalCode: "121001").save(validate: false) + new Market(name: "NIT", address: address).save(deepValidate: false) + + then: + thrown(DataIntegrityViolationException) + + when: "nested validation fails" + address = new Address(streetName: "1B, Main St.", landmark: "V2", postalCode: "11").save(validate: false) + new AnotherCity(name: "Faridabad").addToMarkets(name: "NIT 1", address: address).save(deepValidate: false) + + then: "market is saved, no validation error" + AnotherCity.count() == 1 + Market.count() == 2 + Address.count() == 2 + + when: "invalid embedded object" + new AnotherCity(name: "St. Louis", country: new AnotherCountry()).save(deepValidate: false) + + then: "should save the city" + AnotherCity.count() == 2 + AnotherCountry.count() == 0 + } +} + +@Entity +class AnotherCity { + + String name + AnotherCountry country + + static hasMany = [markets: Market] + static embedded = ['country'] + static constraints = { + country nullable: true + } + +} + + +@Entity +class Market { + + String name + Address address + +} + +@Entity +class Address { + + String streetName + String landmark + String postalCode + + private static final POSTAL_CODE_PATTERN = /^(\d{5}-\d{4})|(\d{5})|(\d{9})$/ + + static constraints = { + streetName nullable: false + landmark nullable: true + postalCode validator: { value -> value ==~ POSTAL_CODE_PATTERN } + } +} + +@Entity +class AnotherCountry { + String name +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/EmbeddedWithValidationExceptionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/EmbeddedWithValidationExceptionSpec.groovy new file mode 100644 index 00000000000..c46c1fd2885 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/EmbeddedWithValidationExceptionSpec.groovy @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.validation + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import grails.validation.ValidationException +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +class EmbeddedWithValidationExceptionSpec extends Specification { + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(DomainWithEmbedded) + + @Rollback + @Issue("https://github.com/grails/grails-data-hibernate5/issues/110") + void "test validation exception with embedded in domain"() { + when: + new DomainWithEmbedded( + foo: 'not valid', + myEmbedded: new MyEmbedded( + a: 1, + b: 'foo' + ) + ).save(failOnError: true) + + then: + thrown(ValidationException) + } +} + +@Entity +class DomainWithEmbedded { + MyEmbedded myEmbedded + String foo + + static embedded = ['myEmbedded'] + + static constraints = { + foo(validator: { val, self -> + return 'not.valid.foo' + }) + } +} + +class MyEmbedded { + Integer a + String b + + static constraints = { + a(nullable: true) + b(nullalbe: true) + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/SaveWithInvalidEntitySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/SaveWithInvalidEntitySpec.groovy new file mode 100644 index 00000000000..dacc15cf819 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/SaveWithInvalidEntitySpec.groovy @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.validation + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 03/05/2017. + */ +class SaveWithInvalidEntitySpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(A, B) + + /** + * This currently fails with a NPE. See explanation https://github.com/apache/grails-core/issues/14616#issuecomment-298943022 + */ + @Rollback + @Ignore + @Issue('https://github.com/apache/grails-core/issues/10604') + void "test save with an invalid entity"() { + when: + hibernateDatastore.currentSession.persist(new A(b:new B(field2: "test"))) + hibernateDatastore.currentSession.flush() + + then: + A.count() == 1 + + } +} + +@Entity +class A { + B b +} +@Entity +class B { + String field1 + String field2 +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/SkipValidationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/SkipValidationSpec.groovy new file mode 100644 index 00000000000..ce60d9ab872 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/SkipValidationSpec.groovy @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.validation + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import grails.validation.ValidationException +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class SkipValidationSpec extends Specification { + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(Author) + + // For whatever reason it may be valid to flush & save without validation (database would obviously fail if the field is too long, but maybe the object is expected to only have an invalid validator?) so continue to support this scenario + @Rollback + void "calling save with flush with validate false should skip validation"() { + when: + new Author(name: 'false').save(failOnError: true, validate: false, flush: true) + + then: + noExceptionThrown() + } + + @Rollback + void "calling save with flush and invalid attribute"() { + when: + new Author(name: 'ThisNameIsTooLong').save(failOnError: true, flush: true) + + then: + thrown(ValidationException) + } + + @Rollback + void "calling validate with property list after save should validate again"() { + // Save but don't flush, this causes the new author to have skipValidate = true + Author author = new Author(name: 'Aaron').save(failOnError: true) + + when: "validate is called again with a property list" + author.name = "ThisNameIsTooLong" + def isValid = author.validate(['name']) + + then: "it should be invalid but it skips validation instead" + !isValid + } + + @Rollback + void "calling validate with property list after save with flush should validate again"() { + // Save but don't flush, this causes the new author to have skipValidate = true + Author author = new Author(name: 'Aaron').save(failOnError: true, flush: true) + + when: "validate is called again with a property list" + author.name = "ThisNameIsTooLong" + def isValid = author.validate(['name']) + + then: "it should be invalid but it skips validation instead" + !isValid + } + + @Rollback + void "calling validate with property list after save should validate again on explicit flush"() { + // Save but don't flush, this causes the new author to have skipValidate = true + Author author = new Author(name: 'Aaron').save(failOnError: true) + + when: "validate is called again with a property list" + author.name = "ThisNameIsTooLong" + Author.withSession { session -> + session.flush() + } + + then: + author.hasErrors() + } + + @Rollback + void "calling validate with no list after save should validate again"() { + // Save but don't flush, this causes the new author to have skipValidate = true + Author author = new Author(name: 'Aaron').save(failOnError: true) + + when: "validate is called again without any parameters" + author.name = "ThisNameIsTooLong" + def isValid = author.validate() + + then: "this works since validate without params doesn't honor skipValidate for some reason" + !isValid + } +} + +@Entity +class Author { + String name + + static constraints = { + name(nullable: false, maxSize: 8, validator: { val, obj -> + if(val == "false") { + return "name.invalid" + } + + println "Validate called" + true + }) + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueFalseConstraintSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueFalseConstraintSpec.groovy new file mode 100644 index 00000000000..6b4655b0ef8 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueFalseConstraintSpec.groovy @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.validation + +import grails.gorm.transactions.Rollback +import grails.gorm.annotation.Entity +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +@Rollback +class UniqueFalseConstraintSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(User) + + @Issue('https://github.com/apache/grails-data-mapping/issues/1059') + void 'unique:false constraint is ignored and does not behave as unique:true'() { + given: 'a user' + def user1 = new User(name: 'John') + user1.save(flush: true) + + when: 'trying to save another user with the same name' + def user2 = new User(name: 'John') + user2.save(flush: true) + + then: 'both users are saved without errors' + !user1.hasErrors() + !user2.hasErrors() + } +} + +@Entity +class User { + Long id + String name + + static constraints = { + name unique: false + } + + static mapping = { + table '`user`' + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueInheritanceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueInheritanceSpec.groovy new file mode 100644 index 00000000000..0cd6450a308 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueInheritanceSpec.groovy @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.validation + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.mapping.reflect.EntityReflector +import org.grails.orm.hibernate.HibernateDatastore +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +@Rollback +class UniqueInheritanceSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(Item, ConcreteProduct, Product, Book) + + void "unique constraint works directly"() { + setup: + Product i = new ConcreteProduct(name: '123') + i.save(flush: true) + + expect: + !i.hasErrors() + + when: + i.save() + + then: + !i.hasErrors() + } + + void "unique constraint works on cascade"() { + setup: + Item i = new Item(product: new ConcreteProduct(name: '123')) + i.save(flush: true) + + expect: + !i.hasErrors() + + when: + i.save() + + then: + !i.hasErrors() // item.product.name is not unique + } + + @Issue('https://github.com/grails/grails-data-hibernate5/issues/32') + void "test save multiple book instances with unique constraint applied"() { + when: + def book1=new Book(title: "one") + book1.save() + def book2=new Book(title: "one") + book2.save() + + then: + book2.hasErrors() + } +} + +@Entity +class Book { + String title + static constraints = { + title (nullable: false,size: 0..200, unique: true, blank:false) + } +} + +@Entity +class Item { + Product product +} + +@Entity +class ConcreteProduct extends Product { + +} + +@Entity +abstract class Product { + String name + + static constraints = { + name unique: true + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithHasOneSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithHasOneSpec.groovy new file mode 100644 index 00000000000..ac4dd977eaa --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithHasOneSpec.groovy @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.validation + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.SessionFactory +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Issue('https://github.com/apache/grails-data-mapping/issues/1004') +class UniqueWithHasOneSpec extends Specification { + + @AutoCleanup @Shared HibernateDatastore hibernateDatastore = new HibernateDatastore(getClass().getPackage()) + @Shared SessionFactory sessionFactory = hibernateDatastore.sessionFactory + + @Rollback + void "test unique constraint with hasOne"() { + when: + Foo foo = new Foo(name: "foo") + Bar bar = new Bar(name: "bar") + foo.bar = bar + bar.foo = foo + foo.save failOnError: true + + then: + Foo.count == 1 + Bar.count == 1 + } +} + +@Entity +class Foo { + + static hasOne = [bar: Bar] + + String name + + static constraints = { + bar nullable: true + name unique: "bar" + } + +} + +@Entity +class Bar { + + static belongsTo = [foo: Foo] + + String name + + static constraints = { + foo nullable: true + } + +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithinGroupSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithinGroupSpec.groovy new file mode 100644 index 00000000000..19b39ab0c35 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithinGroupSpec.groovy @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.tests.validation + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import groovy.transform.EqualsAndHashCode +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.SessionFactory +import org.springframework.dao.DuplicateKeyException +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 29/05/2017. + */ +@Issue('https://github.com/grails/grails-data-hibernate5/issues/36') +class UniqueWithinGroupSpec extends Specification { + + @AutoCleanup + @Shared + HibernateDatastore hibernateDatastore = new HibernateDatastore(getClass().getPackage()) + + @Shared + SessionFactory sessionFactory = hibernateDatastore.sessionFactory + + @Rollback + void "test insert"() { + when: + Thing thing1 = new Thing(hello: 1, world: 2) + thing1.insert(flush: true) + sessionFactory.currentSession.flush() + Thing thing2 = new Thing(hello: 1, world: 2) + thing2.insert(flush: true) + + then: + notThrown(DuplicateKeyException) + !thing1.hasErrors() + thing2.hasErrors() + + } + + @Rollback + void "test save"() { + when: + Thing thing1 = new Thing(hello: 1, world: 2) + thing1.save(insert: true, flush: true) + sessionFactory.currentSession.flush() + Thing thing2 = new Thing(hello: 1, world: 2) + thing2.save(insert: true, flush: true) + + then: + notThrown(DuplicateKeyException) + !thing1.hasErrors() + thing2.hasErrors() + + } + + @Rollback + void "test validate"() { + when: + Thing thing1 = new Thing(hello: 1, world: 2).save(insert: true, flush: true) + sessionFactory.currentSession.flush() + Thing thing2 = new Thing(hello: 1, world: 2) + + then: + !thing1.hasErrors() + !thing2.validate() + thing2.errors.getFieldError('hello').code == 'unique' + } +} + +@Entity +@EqualsAndHashCode(includes = ['hello', 'world']) +class Thing implements Serializable { + Long hello + Long world + + static constraints = { + hello unique: 'world' + } + static mapping = { + version false + id composite: ['hello', 'world'] + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy b/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy new file mode 100644 index 00000000000..7555856f577 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.grails.data.hibernate7.core + +import grails.core.DefaultGrailsApplication +import grails.core.GrailsApplication +import groovy.sql.Sql +import org.apache.grails.data.testing.tck.base.GrailsDataTckManager +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.core.Session +import org.grails.orm.hibernate.GrailsHibernateTransactionManager +import org.grails.orm.hibernate.HibernateDatastore +import org.grails.orm.hibernate.cfg.HibernateMappingContextConfiguration +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings +import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver +import org.h2.Driver +import org.hibernate.SessionFactory +import org.hibernate.dialect.H2Dialect +import org.springframework.beans.factory.DisposableBean +import org.springframework.context.ApplicationContext +import org.grails.orm.hibernate.support.hibernate5.SessionFactoryUtils +import org.grails.orm.hibernate.support.hibernate5.SessionHolder +import org.springframework.transaction.TransactionStatus +import org.springframework.transaction.support.DefaultTransactionDefinition +import org.springframework.transaction.support.TransactionSynchronizationManager +import spock.lang.Specification + +class GrailsDataHibernate7TckManager extends GrailsDataTckManager { + GrailsApplication grailsApplication + HibernateDatastore hibernateDatastore + org.hibernate.Session hibernateSession + GrailsHibernateTransactionManager transactionManager + SessionFactory sessionFactory + TransactionStatus transactionStatus + HibernateMappingContextConfiguration hibernateConfig + ApplicationContext applicationContext + HibernateDatastore multiDataSourceDatastore + HibernateDatastore multiTenantMultiDataSourceDatastore + + @Override + void setup(Class spec) { + cleanRegistry() + super.setup(spec) + } + + @Override + Session createSession() { + ConfigObject grailsConfig = new ConfigObject() + boolean isTransactional = true + + System.setProperty('hibernate7.gorm.suite', "true") + grailsApplication = new DefaultGrailsApplication(domainClasses as Class[], new GroovyClassLoader(GrailsDataHibernate7TckManager.getClassLoader())) + if (grailsConfig) { + grailsApplication.config.putAll(grailsConfig) + } + + grailsConfig.dataSource.dbCreate = "create-drop" + hibernateDatastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(grailsConfig), domainClasses as Class[]) + transactionManager = hibernateDatastore.getTransactionManager() + sessionFactory = hibernateDatastore.sessionFactory + if (transactionStatus == null && isTransactional) { + transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition()) + } else if (isTransactional) { + throw new RuntimeException("new transaction started during active transaction") + } + if (!isTransactional) { + hibernateSession = sessionFactory.openSession() + TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(hibernateSession)) + } else { + hibernateSession = sessionFactory.currentSession + } + + return hibernateDatastore.connect() + } + + @Override + void destroy() { + super.destroy() + + if (transactionStatus != null) { + def tx = transactionStatus + transactionStatus = null + transactionManager.rollback(tx) + } + if (hibernateSession != null) { + SessionFactoryUtils.closeSession( (org.hibernate.Session)hibernateSession ) + } + + if(hibernateConfig != null) { + hibernateConfig = null + } + hibernateDatastore.destroy() + grailsApplication = null + hibernateDatastore = null + hibernateSession = null + transactionManager = null + sessionFactory = null + if(applicationContext instanceof DisposableBean) { + applicationContext.destroy() + } + applicationContext = null + shutdownInMemDb() + } + + @Override + boolean supportsMultipleDataSources() { + true + } + + @Override + void setupMultiDataSource(Class... domainClasses) { + Map config = [ + 'dataSource.url' : "jdbc:h2:mem:tckDefaultDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate' : 'create-drop', + 'dataSource.dialect' : H2Dialect.name, + 'dataSource.formatSql' : 'true', + 'hibernate.flush.mode' : 'COMMIT', + 'hibernate.cache.queries' : 'true', + 'hibernate.hbm2ddl.auto' : 'create-drop', + 'dataSources.secondary' : [url: "jdbc:h2:mem:tckSecondaryDB;LOCK_TIMEOUT=10000"], + ] + multiDataSourceDatastore = new HibernateDatastore( + DatastoreUtils.createPropertyResolver(config), domainClasses + ) + } + + @Override + void cleanupMultiDataSource() { + if (multiDataSourceDatastore != null) { + multiDataSourceDatastore.destroy() + multiDataSourceDatastore = null + shutdownInMemDb('jdbc:h2:mem:tckDefaultDB') + shutdownInMemDb('jdbc:h2:mem:tckSecondaryDB') + } + } + + @Override + def getServiceForConnection(Class serviceType, String connectionName) { + multiDataSourceDatastore + .getDatastoreForConnection(connectionName) + .getService(serviceType) + } + + @Override + boolean supportsMultiTenantMultiDataSource() { + true + } + + @Override + void setupMultiTenantMultiDataSource(Class... domainClasses) { + Map config = [ + 'grails.gorm.multiTenancy.mode' : MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR, + 'grails.gorm.multiTenancy.tenantResolverClass': SystemPropertyTenantResolver, + 'dataSource.url' : "jdbc:h2:mem:tckMtDefaultDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate' : 'create-drop', + 'dataSource.dialect' : H2Dialect.name, + 'dataSource.formatSql' : 'true', + 'hibernate.flush.mode' : 'COMMIT', + 'hibernate.cache.queries' : 'true', + 'hibernate.hbm2ddl.auto' : 'create-drop', + 'dataSources.secondary' : [url: "jdbc:h2:mem:tckMtSecondaryDB;LOCK_TIMEOUT=10000"], + ] + multiTenantMultiDataSourceDatastore = new HibernateDatastore( + DatastoreUtils.createPropertyResolver(config), domainClasses + ) + } + + @Override + void cleanupMultiTenantMultiDataSource() { + if (multiTenantMultiDataSourceDatastore != null) { + multiTenantMultiDataSourceDatastore.destroy() + multiTenantMultiDataSourceDatastore = null + shutdownInMemDb('jdbc:h2:mem:tckMtDefaultDB') + shutdownInMemDb('jdbc:h2:mem:tckMtSecondaryDB') + } + } + + @Override + def getServiceForMultiTenantConnection(Class serviceType, String connectionName) { + multiTenantMultiDataSourceDatastore + .getDatastoreForConnection(connectionName) + .getService(serviceType) + } + + private void shutdownInMemDb() { + shutdownInMemDb('jdbc:h2:mem:grailsDb') + } + + private void shutdownInMemDb(String url) { + Sql sql = null + try { + sql = Sql.newInstance(url, 'sa', '', Driver.name) + sql.executeUpdate('SHUTDOWN') + } catch (e) { + // already closed, ignore + } finally { + try { sql?.close() } catch (ignored) {} + } + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/DefaultConstraintsSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/DefaultConstraintsSpec.groovy new file mode 100644 index 00000000000..44bc1c66b22 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/DefaultConstraintsSpec.groovy @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.cfg.Settings +import org.springframework.core.env.PropertyResolver +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 13/09/2016. + */ +class DefaultConstraintsSpec extends Specification { + + @Shared PropertyResolver configuration = DatastoreUtils.createPropertyResolver( + (Settings.SETTING_DB_CREATE):'create', + 'grails.gorm.default.constraints':{ + '*'(nullable: true) + } + ) + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(configuration,Book) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.getTransactionManager() + + @Rollback + @Issue('https://github.com/apache/grails-data-mapping/issues/746') + void "Test that when constraints are nullable true by default, they can be altered to nullable false"() { + when:"An object is validated" + Book book = new Book() + book.validate() + + then:"It has errors" + book.hasErrors() + book.errors.getFieldError("title") + + when:"The title is set" + book.title = "The Stand" + book.clearErrors() + book.validate() + + then:"It validates" + !book.hasErrors() + + when:"Validation is bypassed" + book.title = null + book.save(validate:false) + + then:"A constraint violation exception is thrown" + Book.count() == 0 + thrown DataIntegrityViolationException + } +} + +@Entity +class Book { + String title + String author + + static constraints = { + title nullable:false + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ExistsCrossJoinSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ExistsCrossJoinSpec.groovy new file mode 100644 index 00000000000..a0365a13e88 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ExistsCrossJoinSpec.groovy @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.cfg.Settings +import org.hibernate.resource.jdbc.spi.StatementInspector +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +@Issue('https://github.com/apache/grails-core/issues/14334') +class ExistsCrossJoinSpec extends Specification { + + @Shared SqlCapture sqlCapture = new SqlCapture() + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore( + DatastoreUtils.createPropertyResolver( + (Settings.SETTING_DB_CREATE): 'create-drop', + 'hibernate.session_factory.statement_inspector': sqlCapture + ), + ExistsItem + ) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.getTransactionManager() + + @Rollback + void "exists returns true for existing entity"() { + given: + ExistsItem item = new ExistsItem(name: 'alpha').save(flush: true) + + expect: + ExistsItem.exists(item.id) + } + + @Rollback + void "exists returns false for non-existent id"() { + expect: + !ExistsItem.exists(99999) + } + + @Rollback + void "exists does not produce a cross-join"() { + given: + new ExistsItem(name: 'one').save(flush: true) + new ExistsItem(name: 'two').save(flush: true) + new ExistsItem(name: 'three').save(flush: true) + + when: + sqlCapture.clear() + ExistsItem item = new ExistsItem(name: 'target').save(flush: true) + sqlCapture.clear() + ExistsItem.exists(item.id) + + then: "the SQL should contain only a single FROM clause (no cross-join)" + sqlCapture.statements.any { it.toLowerCase().contains('select count') } + + and: "there should be exactly one table reference in the FROM clause" + String countSql = sqlCapture.statements.find { it.toLowerCase().contains('select count') } + countSql != null + // A cross-join would have the table name appearing twice after 'from' + // e.g. "from exists_item x0_, exists_item x1_" vs correct "from exists_item x0_" + countSql.toLowerCase().split('cross join').length == 1 + // Verify no comma-join pattern (two table aliases after FROM) + !countSql.toLowerCase().matches(/.*from\s+\S+\s+\S+\s*,\s*\S+\s+\S+.*/) + } + + @Rollback + void "exists with multiple rows returns correct result"() { + given: "multiple entities in the table" + ExistsItem target = new ExistsItem(name: 'target').save(flush: true) + new ExistsItem(name: 'other1').save(flush: true) + new ExistsItem(name: 'other2').save(flush: true) + new ExistsItem(name: 'other3').save(flush: true) + new ExistsItem(name: 'other4').save(flush: true) + + expect: "exists returns correct results" + ExistsItem.exists(target.id) + !ExistsItem.exists(99999) + } + + /** + * Captures SQL statements executed by Hibernate for inspection in tests. + */ + static class SqlCapture implements StatementInspector { + final List statements = Collections.synchronizedList(new ArrayList()) + + @Override + String inspect(String sql) { + statements.add(sql) + return sql + } + + void clear() { + statements.clear() + } + } +} + +@Entity +class ExistsItem { + String name +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateDatastoreSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateDatastoreSpec.groovy new file mode 100644 index 00000000000..6b320da48a3 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateDatastoreSpec.groovy @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate + +import org.grails.orm.hibernate.cfg.Settings +import spock.lang.Specification + +/** + * Created by graemerocher on 22/09/2016. + */ +class HibernateDatastoreSpec extends Specification { + + void "test configure via map"() { + when:"The map constructor is used" + def config = Collections.singletonMap(Settings.SETTING_DB_CREATE, "create-drop") + HibernateDatastore datastore = new HibernateDatastore(config, Book) + + then:"GORM is configured correctly" + Book.withNewSession { + Book.count() + } == 0 + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextSpec.groovy new file mode 100644 index 00000000000..4a1a1940f2c --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextSpec.groovy @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg + +import grails.gorm.annotation.Entity +import org.grails.datastore.mapping.engine.types.AbstractMappingAwareCustomTypeMarshaller +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.ValueGenerator +import org.grails.orm.hibernate.connections.HibernateConnectionSourceSettings +import spock.lang.Specification + +/** + * Created by graemerocher on 07/10/2016. + */ +class HibernateMappingContextSpec extends Specification { + + void "test entity with custom id generator"() { + when:"A context is created" + def mappingContext = new HibernateMappingContext() + PersistentEntity entity = mappingContext.addPersistentEntity(CustomIdGeneratorEntity) + + then:"The mapping is correct" + entity.mapping.identifier.generator == ValueGenerator.CUSTOM + } + + void "test entity with custom type marshaller is registered correctly"() { + given:"A configured custom type marshaller" + HibernateConnectionSourceSettings settings = new HibernateConnectionSourceSettings() + settings.custom.types = [new MyTypeMarshaller(MyUUIDGenerator)] + + when:"A context is created" + def mappingContext = new HibernateMappingContext(settings) + + then:"The mapping is created successfully" + mappingContext + mappingContext.mappingFactory + + and:"The type is registered as a custom type with the mapping factory" + mappingContext.mappingFactory.isCustomType(MyUUIDGenerator) + } +} + +@Entity +class CustomIdGeneratorEntity { + String name + static mapping = { + id(generator: "org.grails.orm.hibernate.cfg.MyUUIDGenerator", type: "uuid-binary") + } +} + +class MyUUIDGenerator { +} + +class MyTypeMarshaller extends AbstractMappingAwareCustomTypeMarshaller { + MyTypeMarshaller(Class targetType) { + super(targetType) + } + @Override + protected Object writeInternal(PersistentProperty property, String key, Object value, Object nativeTarget) { + return value + } + @Override + protected Object readInternal(PersistentProperty property, String key, Object nativeSource) { + return nativeSource + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/compiler/HibernateEntityTransformationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/compiler/HibernateEntityTransformationSpec.groovy new file mode 100644 index 00000000000..c5c6d681dad --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/compiler/HibernateEntityTransformationSpec.groovy @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.compiler + +import groovy.transform.Generated +import org.hibernate.engine.spi.EntityEntry +import org.hibernate.engine.spi.ManagedEntity +import org.hibernate.engine.spi.PersistentAttributeInterceptable +import org.hibernate.engine.spi.PersistentAttributeInterceptor +import spock.lang.Specification + +/** + * Created by graemerocher on 15/11/16. + */ +class HibernateEntityTransformationSpec extends Specification { + + void "test hibernate entity transformation"() { + when:"A hibernate interceptor is set" + Class cls = new GroovyClassLoader().parseClass(''' +import grails.gorm.hibernate.annotation.ManagedEntity +@ManagedEntity +class MyEntity { + String name + String lastName + int age + + String getLastName() { + return this.lastName + } + + void setLastName(String name) { + this.lastName = name + } +} +''') + then: + PersistentAttributeInterceptable.isAssignableFrom(cls) + ManagedEntity.isAssignableFrom(cls) + + when: + Object myEntity = cls.newInstance() + + ((PersistentAttributeInterceptable)myEntity).$$_hibernate_setInterceptor( + new PersistentAttributeInterceptor() { + @Override + boolean readBoolean(Object obj, String name, boolean oldValue) { + + + } + + @Override + boolean writeBoolean(Object obj, String name, boolean oldValue, boolean newValue) { + return false + } + + @Override + byte readByte(Object obj, String name, byte oldValue) { + return 0 + } + + @Override + byte writeByte(Object obj, String name, byte oldValue, byte newValue) { + return 0 + } + + @Override + char readChar(Object obj, String name, char oldValue) { + return 0 + } + + @Override + char writeChar(Object obj, String name, char oldValue, char newValue) { + return 0 + } + + @Override + short readShort(Object obj, String name, short oldValue) { + return 0 + } + + @Override + short writeShort(Object obj, String name, short oldValue, short newValue) { + return 0 + } + + @Override + int readInt(Object obj, String name, int oldValue) { + return 10 + } + + @Override + int writeInt(Object obj, String name, int oldValue, int newValue) { + return 10 + } + + @Override + float readFloat(Object obj, String name, float oldValue) { + return 0 + } + + @Override + float writeFloat(Object obj, String name, float oldValue, float newValue) { + return 0 + } + + @Override + double readDouble(Object obj, String name, double oldValue) { + return 0 + } + + @Override + double writeDouble(Object obj, String name, double oldValue, double newValue) { + return 0 + } + + @Override + long readLong(Object obj, String name, long oldValue) { + return 0 + } + + @Override + long writeLong(Object obj, String name, long oldValue, long newValue) { + return 0 + } + + @Override + Object readObject(Object obj, String name, Object oldValue) { + return "good" + } + + @Override + Object writeObject(Object obj, String name, Object oldValue, Object newValue) { + return "changed" + } + + @Override + Set getInitializedLazyAttributeNames() { + return Collections.emptySet() + } + + @Override + void attributeInitialized(String name) { + + } + } + ) + + then:"the interceptor is used when reading a property" + myEntity.name == 'good' + myEntity.lastName == 'good' + myEntity.age == 10 + + when:"A setter is set" + myEntity.name = 'something' + myEntity.age = 5 + ((PersistentAttributeInterceptable)myEntity).$$_hibernate_setInterceptor( null ) + + then:"The value is changed" + myEntity.name == 'changed' + + and: "by transformation added methods are all marked as Generated" + cls.getMethod('$$_hibernate_getInterceptor').isAnnotationPresent(Generated) + cls.getMethod('$$_hibernate_setInterceptor', PersistentAttributeInterceptor).isAnnotationPresent(Generated) + cls.getMethod('$$_hibernate_getEntityInstance').isAnnotationPresent(Generated) + cls.getMethod('$$_hibernate_getEntityEntry').isAnnotationPresent(Generated) + cls.getMethod('$$_hibernate_setEntityEntry', EntityEntry).isAnnotationPresent(Generated) + cls.getMethod('$$_hibernate_getPreviousManagedEntity').isAnnotationPresent(Generated) + cls.getMethod('$$_hibernate_getNextManagedEntity').isAnnotationPresent(Generated) + cls.getMethod('$$_hibernate_setPreviousManagedEntity', ManagedEntity).isAnnotationPresent(Generated) + cls.getMethod('$$_hibernate_setNextManagedEntity', ManagedEntity).isAnnotationPresent(Generated) + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceDatasourceInheritanceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceDatasourceInheritanceSpec.groovy new file mode 100644 index 00000000000..66946174687 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceDatasourceInheritanceSpec.groovy @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import org.hibernate.dialect.H2Dialect +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import grails.gorm.annotation.Entity +import grails.gorm.services.Service +import grails.gorm.transactions.Transactional + +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.HibernateDatastore + +class DataServiceDatasourceInheritanceSpec extends Specification { + + @Shared Map config = [ + 'dataSource.url': 'jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000', + 'dataSource.dbCreate': 'create-drop', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create-drop', + 'dataSources.warehouse': [ + url: 'jdbc:h2:mem:warehouseDB;LOCK_TIMEOUT=10000' + ], + ] + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore( + DatastoreUtils.createPropertyResolver(config), Inventory + ) + + @Shared InventoryService inventoryService + @Shared InventoryService defaultDatastoreInventoryService + @Shared InventoryDataService inventoryDataService + + void setupSpec() { + inventoryService = datastore + .getDatastoreForConnection('warehouse') + .getService(InventoryService) + defaultDatastoreInventoryService = datastore + .getService(InventoryService) + inventoryDataService = datastore + .getDatastoreForConnection('warehouse') + .getService(InventoryDataService) + } + + void setup() { + Inventory.warehouse.withNewTransaction { + Inventory.warehouse.executeUpdate('delete from Inventory') + } + } + + void "abstract service without @Transactional(connection) inherits from domain"() { + when: "saving through a service that has no @Transactional(connection)" + def saved = inventoryService.save(new Inventory(sku: 'ABC-001', quantity: 50)) + + then: "the entity is persisted on the warehouse datasource" + saved != null + saved.id != null + saved.sku == 'ABC-001' + + and: "it exists on the warehouse datasource" + Inventory.warehouse.withNewTransaction { + Inventory.warehouse.count() + } == 1 + } + + void "service obtained from default datastore still routes to inherited datasource"() { + when: "saving through a service obtained from the default datastore" + def saved = defaultDatastoreInventoryService.save(new Inventory(sku: 'DEFAULT-001', quantity: 33)) + + then: "the entity is persisted" + saved != null + saved.id != null + + and: "it exists on the warehouse datasource" + Inventory.warehouse.withNewTransaction { + Inventory.warehouse.count() + } == 1 + + and: "retrievable through the same service" + defaultDatastoreInventoryService.get(saved.id) != null + } + + void "get by ID routes to inherited datasource"() { + given: "an inventory item saved on warehouse" + def saved = inventoryService.save(new Inventory(sku: 'GET-001', quantity: 10)) + + when: "retrieving by ID" + def found = inventoryService.get(saved.id) + + then: "the correct entity is returned" + found != null + found.id == saved.id + found.sku == 'GET-001' + } + + void "delete routes to inherited datasource"() { + given: "an inventory item saved on warehouse" + def saved = inventoryService.save(new Inventory(sku: 'DEL-001', quantity: 5)) + + when: "deleting by ID" + def deleted = inventoryService.delete(saved.id) + + then: "the entity is deleted" + deleted != null + deleted.sku == 'DEL-001' + inventoryService.get(saved.id) == null + } + + void "count routes to inherited datasource"() { + given: "items saved on warehouse" + inventoryService.save(new Inventory(sku: 'CNT-001', quantity: 1)) + inventoryService.save(new Inventory(sku: 'CNT-002', quantity: 2)) + + expect: "count returns 2" + inventoryService.count() == 2 + } + + void "findBySku routes to inherited datasource"() { + given: "items saved on warehouse" + inventoryService.save(new Inventory(sku: 'FIND-001', quantity: 100)) + + when: "finding by sku" + def found = inventoryService.findBySku('FIND-001') + + then: "the correct entity is returned" + found != null + found.sku == 'FIND-001' + found.quantity == 100 + } + + void "interface service inherits datasource from domain"() { + when: "saving through an interface service with no @Transactional(connection)" + def saved = inventoryDataService.save(new Inventory(sku: 'IFACE-001', quantity: 25)) + + then: "the entity is persisted on warehouse" + saved != null + saved.id != null + + and: "retrievable through the same service" + inventoryDataService.get(saved.id) != null + } + + void "explicit @Transactional(connection) is preserved and not overwritten by domain datasource"() { + when: "checking the annotation on a service with explicit @Transactional(connection='archive')" + def transactionalAnn = ExplicitArchiveInventoryService.getAnnotation(Transactional) + + then: "the explicit connection value 'archive' is preserved, not overwritten with domain's 'warehouse'" + transactionalAnn != null + transactionalAnn.connection() == 'archive' + + and: "the inherited service uses the domain's 'warehouse' connection" + def inheritedAnn = InventoryService.getAnnotation(Transactional) + inheritedAnn != null + inheritedAnn.connection() == 'warehouse' + } + + void "abstract and interface services share the same inherited datasource"() { + given: "an item saved through the abstract service" + def saved = inventoryService.save(new Inventory(sku: 'CROSS-001', quantity: 42)) + + expect: "the interface service can find it" + inventoryDataService.findBySku('CROSS-001') != null + inventoryDataService.findBySku('CROSS-001').id == saved.id + } + +} + +@Entity +class Inventory { + + Long id + Long version + String sku + Integer quantity + + static mapping = { + datasource('warehouse') + } + static constraints = { + sku(blank: false) + } +} + +@Service(Inventory) +abstract class InventoryService { + + abstract Inventory get(Serializable id) + + abstract Inventory save(Inventory item) + + abstract Inventory delete(Serializable id) + + abstract Number count() + + abstract Inventory findBySku(String sku) +} + +@Service(Inventory) +interface InventoryDataService { + + Inventory get(Serializable id) + + Inventory save(Inventory item) + + Inventory delete(Serializable id) + + Inventory findBySku(String sku) +} + +@Service(Inventory) +@Transactional(connection = 'archive') +abstract class ExplicitArchiveInventoryService { + + abstract Inventory get(Serializable id) + + abstract Inventory save(Inventory item) +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiDataSourceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiDataSourceSpec.groovy new file mode 100644 index 00000000000..776a27d6643 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiDataSourceSpec.groovy @@ -0,0 +1,460 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import org.hibernate.dialect.H2Dialect +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import grails.gorm.annotation.Entity +import grails.gorm.services.Query +import grails.gorm.services.Service +import grails.gorm.transactions.Transactional +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.HibernateDatastore + +/** + * Integration tests for GORM Data Service auto-implemented CRUD methods + * routing to a non-default datasource via @Transactional(connection). + * + * The Product domain is mapped exclusively to the 'books' datasource. + * Without the connection-routing fix, auto-implemented save/get/delete + * would attempt to use the default datasource (where no Product table + * exists), causing failures. + * + * Tests both patterns: + * - Abstract class implementing interface (ProductService) + * - Interface-only with @Transactional(connection) (ProductDataService) + * + * @see org.grails.datastore.gorm.services.implementers.SaveImplementer + * @see org.grails.datastore.gorm.services.implementers.DeleteImplementer + * @see org.grails.datastore.gorm.services.implementers.FindAndDeleteImplementer + * @see org.grails.datastore.gorm.services.implementers.AbstractDetachedCriteriaServiceImplementor + */ +class DataServiceMultiDataSourceSpec extends Specification { + + @Shared Map config = [ + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'create-drop', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create-drop', + 'dataSources.books':[url:"jdbc:h2:mem:booksDB;LOCK_TIMEOUT=10000"], + ] + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore( + DatastoreUtils.createPropertyResolver(config), Product + ) + + @Shared ProductService productService + @Shared ProductDataService productDataService + + void setupSpec() { + productService = datastore + .getDatastoreForConnection('books') + .getService(ProductService) + productDataService = datastore + .getDatastoreForConnection('books') + .getService(ProductDataService) + } + + void setup() { + productService.deleteAll() + } + + void "schema is created on the books datasource"() { + expect: 'Product table exists on books - count succeeds without exception' + productService.count() == 0 + } + + void "save routes to books datasource"() { + when: 'a product is saved through the Data Service' + def saved = productService.save(new Product(name: 'Widget', amount: 42)) + + then: 'it is persisted with an ID' + saved != null + saved.id != null + saved.name == 'Widget' + saved.amount == 42 + + and: 'it exists on the books datasource' + productService.count() == 1 + } + + void "get by ID routes to books datasource"() { + given: 'a product saved on books' + def saved = productService.save(new Product(name: 'Gadget', amount: 99)) + + when: 'we retrieve it by ID' + def found = productService.get(saved.id) + + then: 'the correct entity is returned' + found != null + found.id == saved.id + found.name == 'Gadget' + found.amount == 99 + } + + void "count routes to books datasource"() { + given: 'two products saved on books' + productService.save(new Product(name: 'Alpha', amount: 10)) + productService.save(new Product(name: 'Beta', amount: 20)) + + expect: 'count returns 2' + productService.count() == 2 + } + + void "delete by ID routes to books datasource - FindAndDeleteImplementer"() { + given: 'a product saved on books' + def saved = productService.save(new Product(name: 'Ephemeral', amount: 1)) + + when: 'we delete it using delete(id) which returns the domain object' + def deleted = productService.delete(saved.id) + + then: 'the deleted entity is returned and no longer exists' + deleted != null + deleted.name == 'Ephemeral' + productService.get(saved.id) == null + productService.count() == 0 + } + + void "delete by ID routes to books datasource - DeleteImplementer"() { + given: 'a product saved on books' + def saved = productService.save(new Product(name: 'AlsoEphemeral', amount: 2)) + + when: 'we delete it using void deleteProduct(id)' + productService.deleteProduct(saved.id) + + then: 'it no longer exists' + productService.get(saved.id) == null + productService.count() == 0 + } + + void "findByName routes to books datasource"() { + given: "products saved on books" + productService.save(new Product(name: 'Unique', amount: 77)) + productService.save(new Product(name: 'Other', amount: 88)) + + when: "we find by name" + def found = productService.findByName('Unique') + + then: "the correct entity is returned" + found != null + found.name == 'Unique' + found.amount == 77 + } + + void "findAllByName routes to books datasource"() { + given: 'products with duplicate names on books' + productService.save(new Product(name: 'Duplicate', amount: 10)) + productService.save(new Product(name: 'Duplicate', amount: 20)) + productService.save(new Product(name: 'Singleton', amount: 30)) + + when: 'we find all by name' + def found = productService.findAllByName('Duplicate') + + then: 'both matching entities are returned' + found.size() == 2 + found.every { it.name == 'Duplicate' } + } + + void "@Query aggregate works on books datasource"() { + given: 'products saved on books' + productService.save(new Product(name: 'Foo', amount: 100)) + productService.save(new Product(name: 'Bar', amount: 200)) + + when: 'we run an aggregate @Query through the data service' + def total = productService.getTotalAmount() + + then: 'the aggregation reflects books data' + total == 300 + } + + void "save, get, and find round-trip through Data Service"() { + when: 'a product is saved, retrieved by ID, and found by name' + def saved = productService.save(new Product(name: 'RoundTrip', amount: 33)) + def byId = productService.get(saved.id) + def byName = productService.findByName('RoundTrip') + + then: 'all three references point to the same entity' + saved.id == byId.id + saved.id == byName.id + byId.name == 'RoundTrip' + byName.amount == 33 + } + + void "save with constructor-style arguments routes to books datasource"() { + when: 'a product is saved using property arguments' + def saved = productService.saveProduct('Constructed', 55) + + then: 'it is persisted on books' + saved != null + saved.id != null + saved.name == 'Constructed' + saved.amount == 55 + + and: 'retrievable' + productService.get(saved.id) != null + } + + // ---- Interface-pattern Data Service tests ---- + + void "interface service: save routes to books datasource"() { + when: 'a product is saved through the interface Data Service' + def saved = productDataService.save(new Product(name: 'InterfaceWidget', amount: 42)) + + then: 'it is persisted with an ID' + saved != null + saved.id != null + saved.name == 'InterfaceWidget' + saved.amount == 42 + + and: 'it exists on the books datasource' + productDataService.count() == 1 + } + + void "interface service: get by ID routes to books datasource"() { + given: 'a product saved on books via abstract service' + def saved = productService.save(new Product(name: 'InterfaceGet', amount: 99)) + + when: 'we retrieve it through the interface Data Service' + def found = productDataService.get(saved.id) + + then: 'the correct entity is returned' + found != null + found.id == saved.id + found.name == 'InterfaceGet' + } + + void "interface service: delete routes to books datasource"() { + given: 'a product saved on books' + def saved = productService.save(new Product(name: 'InterfaceDelete', amount: 1)) + + when: 'we delete through the interface Data Service (FindAndDeleteImplementer)' + def deleted = productDataService.delete(saved.id) + + then: 'the entity is deleted' + deleted != null + deleted.name == 'InterfaceDelete' + productDataService.get(saved.id) == null + } + + void "interface service: void delete routes to books datasource"() { + given: 'a product saved on books' + def saved = productService.save(new Product(name: 'InterfaceVoidDel', amount: 2)) + + when: 'we delete through the interface Data Service (DeleteImplementer)' + productDataService.deleteProduct(saved.id) + + then: 'the entity is deleted' + productDataService.get(saved.id) == null + } + + void "interface and abstract services share the same datasource"() { + given: 'a product saved through the abstract service' + def saved = productService.save(new Product(name: 'CrossService', amount: 77)) + + expect: 'the interface service can find it and vice versa' + productDataService.findByName('CrossService') != null + productDataService.findByName('CrossService').id == saved.id + + and: 'counts match across both service patterns' + productService.count() == productDataService.count() + } + + void "@Query find-one routes to books datasource - abstract service"() { + given: 'a product saved on books' + productService.save(new Product(name: 'QueryOne', amount: 50)) + + when: 'we find one by HQL query' + def found = productService.findOneByQuery('QueryOne') + + then: 'the correct entity is returned from books' + found != null + found.name == 'QueryOne' + found.amount == 50 + } + + void "@Query find-one returns null for non-existent - abstract service"() { + expect: 'null for non-existent product' + productService.findOneByQuery('NonExistent') == null + } + + void "@Query find-all routes to books datasource - abstract service"() { + given: 'products saved on books with varying amounts' + productService.save(new Product(name: 'Expensive1', amount: 500)) + productService.save(new Product(name: 'Expensive2', amount: 600)) + productService.save(new Product(name: 'Cheap1', amount: 10)) + + when: 'we find all by HQL query with threshold' + def found = productService.findAllByQuery(400) + + then: 'only matching products from books are returned' + found.size() == 2 + found*.name.containsAll(['Expensive1', 'Expensive2']) + } + + void "@Query update routes to books datasource - abstract service"() { + given: 'a product saved on books' + productService.save(new Product(name: 'UpdateTarget', amount: 100)) + + when: 'we update amount by HQL query' + def updated = productService.updateAmountByName('UpdateTarget', 999) + + then: 'one row updated' + updated == 1 + + and: 'the change is reflected on books' + productService.findByName('UpdateTarget').amount == 999 + } + + void "@Query find-one routes to books datasource - interface service"() { + given: 'a product saved on books' + productService.save(new Product(name: 'InterfaceQueryOne', amount: 75)) + + when: 'we find one by HQL query through the interface service' + def found = productDataService.findOneByQuery('InterfaceQueryOne') + + then: 'the correct entity is returned from books' + found != null + found.name == 'InterfaceQueryOne' + found.amount == 75 + } + + void "@Query find-all routes to books datasource - interface service"() { + given: 'products saved on books' + productService.save(new Product(name: 'IfaceExpensive1', amount: 500)) + productService.save(new Product(name: 'IfaceExpensive2', amount: 600)) + productService.save(new Product(name: 'IfaceCheap1', amount: 10)) + + when: 'we find all by HQL query through the interface service' + def found = productDataService.findAllByQuery(400) + + then: 'only matching products from books are returned' + found.size() == 2 + found*.name.containsAll(['IfaceExpensive1', 'IfaceExpensive2']) + } + + void "@Query update routes to books datasource - interface service"() { + given: 'a product saved on books' + productService.save(new Product(name: 'InterfaceUpdate', amount: 100)) + + when: 'we update amount by HQL query through the interface service' + def updated = productDataService.updateAmountByName('InterfaceUpdate', 888) + + then: 'one row updated' + updated == 1 + + and: 'the change is reflected on books' + productDataService.findByName('InterfaceUpdate').amount == 888 + } + +} + +@Entity +class Product { + Long id + Long version + String name + Integer amount + + static mapping = { + datasource 'books' + } + static constraints = { + name blank: false + } +} + +@Service(Product) +@Transactional(connection = 'books') +abstract class ProductService { + + abstract Product get(Serializable id) + + abstract Product save(Product product) + + abstract Product delete(Serializable id) + + abstract void deleteProduct(Serializable id) + + abstract Number count() + + abstract Product findByName(String name) + + abstract List findAllByName(String name) + + @Query("delete from ${Product p} where 1=1") + abstract Number deleteAll() + + @Query("select sum(p.amount) from ${Product p}") + abstract Number getTotalAmount() + + /** + * Constructor-style save - GORM creates the entity from parameters. + * Tests that SaveImplementer routes multi-arg saves through connection-aware API. + */ + abstract Product saveProduct(String name, Integer amount) + + @Query("from ${Product p} where $p.name = $name") + abstract Product findOneByQuery(String name) + + + @Query("from ${Product p} where $p.amount >= $minAmount") + abstract List findAllByQuery(Integer minAmount) + + @Query("update ${Product p} set $p.amount = $newAmount where $p.name = $name") + abstract Number updateAmountByName(String name, Integer newAmount) +} + +/** + * Interface-only Data Service pattern. + * Verifies that connection routing works identically whether the service + * is declared as an interface or an abstract class. + */ +@Service(Product) +@Transactional(connection = 'books') +interface ProductDataService { + + Product get(Serializable id) + + Product save(Product product) + + Product delete(Serializable id) + + void deleteProduct(Serializable id) + + Number count() + + Product findByName(String name) + + List findAllByName(String name) + + @Query("from ${Product p} where $p.name = $name") + Product findOneByQuery(String name) + + @Query("from ${Product p} where $p.amount >= $minAmount") + List findAllByQuery(Integer minAmount) + + @Query("update ${Product p} set $p.amount = $newAmount where $p.name = $name") + Number updateAmountByName(String name, Integer newAmount) +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy new file mode 100644 index 00000000000..f4f01de431d --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy @@ -0,0 +1,287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import org.hibernate.Session +import org.hibernate.dialect.H2Dialect +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.environment.RestoreSystemProperties + +import grails.gorm.MultiTenant +import grails.gorm.annotation.Entity +import grails.gorm.services.Service +import grails.gorm.transactions.Transactional +import org.grails.datastore.gorm.GormEntity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings +import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver +import org.grails.orm.hibernate.HibernateDatastore + +/** + * Tests GORM Data Service auto-implemented CRUD methods when both DISCRIMINATOR + * multi-tenancy and a non-default datasource are configured on the same domain. + * + * This combination triggers the allQualifiers() bug: when MultiTenant is present, + * allQualifiers() returns tenant IDs instead of datasource names, causing schema + * creation and query routing to go to the wrong database. + * + * Covers: + * - Schema creation on the correct (analytics) datasource for MultiTenant domains + * - save(), get(), delete(), count() with tenant isolation on secondary datasource + * - findBy* dynamic finders with tenant isolation on secondary datasource + * - Data Service aggregate HQL on secondary datasource + * - Tenant isolation: same-named data under different tenants stays separate + * + * @see PartitionedMultiTenancySpec for basic DISCRIMINATOR multi-tenancy + * @see MultipleDataSourceConnectionsSpec for Data Services on secondary datasource without multi-tenancy + */ +@RestoreSystemProperties +class DataServiceMultiTenantMultiDataSourceSpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore + + void setupSpec() { + Map config = [ + "grails.gorm.multiTenancy.mode": MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR, + "grails.gorm.multiTenancy.tenantResolverClass": SystemPropertyTenantResolver, + 'dataSource.url': "jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'create-drop', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create-drop', + 'dataSources.analytics': [url: "jdbc:h2:mem:analyticsDB;LOCK_TIMEOUT=10000"], + ] + + datastore = new HibernateDatastore( + DatastoreUtils.createPropertyResolver(config), Metric) + } + + MetricService metricService + + void setup() { + tenant = 'tenant1' + metricService = datastore + .getDatastoreForConnection('analytics') + .getService(MetricService) + metricService.deleteAll() + // Also clean tenant2 data + tenant = 'tenant2' + metricService.deleteAll() + // Reset to tenant1 for tests + tenant = 'tenant1' + } + + void "schema is created on analytics datasource"() { + expect: 'The analytics datasource connects to the analyticsDB H2 database' + Metric.analytics.withNewSession { Session s -> + assert s.connection().metaData.getURL() == 'jdbc:h2:mem:analyticsDB' + return true + } + + and: 'The default datasource connects to a different database' + datastore.withNewSession { Session s -> + assert s.connection().metaData.getURL() == 'jdbc:h2:mem:grailsDB' + return true + } + } + + void "save routes to analytics datasource with tenant isolation"() { + when: 'A metric is saved under tenant1' + def saved = metricService.save(new Metric(name: 'page_views', amount: 100)) + + then: 'The metric is persisted with an ID' + saved != null + saved.id != null + saved.name == 'page_views' + saved.amount == 100 + + and: 'The metric is retrievable via the analytics datasource qualifier' + Metric.analytics.withNewSession { + Metric.analytics.get(saved.id) != null + } + } + + void "get retrieves from analytics datasource"() { + given: 'A metric saved to the analytics datasource' + def saved = metricService.save(new Metric(name: 'sessions', amount: 42)) + + when: 'The metric is retrieved by ID' + def found = metricService.get(saved.id) + + then: 'The correct metric is returned' + found != null + found.id == saved.id + found.name == 'sessions' + found.amount == 42 + } + + void "count returns count scoped to current tenant"() { + given: 'Metrics saved under tenant1' + metricService.save(new Metric(name: 'alpha', amount: 1)) + metricService.save(new Metric(name: 'beta', amount: 2)) + + and: 'Metrics saved under tenant2' + tenant = 'tenant2' + metricService.save(new Metric(name: 'gamma', amount: 3)) + + when: 'Counting under tenant1' + tenant = 'tenant1' + def count1 = metricService.count() + + and: 'Counting under tenant2' + tenant = 'tenant2' + def count2 = metricService.count() + + then: 'Each tenant sees only its own data' + count1 == 2 + count2 == 1 + } + + void "delete removes from analytics datasource"() { + given: 'A metric saved under tenant1' + def saved = metricService.save(new Metric(name: 'disposable', amount: 0)) + def id = saved.id + + when: 'The metric is deleted' + metricService.delete(id) + + then: 'The metric is no longer retrievable' + metricService.get(id) == null + metricService.count() == 0 + } + + void "findByName routes to analytics datasource with tenant isolation"() { + given: 'Same-named metrics under different tenants' + metricService.save(new Metric(name: 'shared_name', amount: 100)) + tenant = 'tenant2' + metricService.save(new Metric(name: 'shared_name', amount: 200)) + + when: 'Finding by name under tenant1' + tenant = 'tenant1' + def found1 = metricService.findByName('shared_name') + + and: 'Finding by name under tenant2' + tenant = 'tenant2' + def found2 = metricService.findByName('shared_name') + + then: 'Each tenant gets its own metric' + found1 != null + found1.amount == 100 + + found2 != null + found2.amount == 200 + } + + void "analytics datasource is registered and functional for MultiTenant entity"() { + when: 'A metric is saved via the data service (routes to analytics datasource)' + def saved = metricService.save(new Metric(name: 'registration-check', amount: 1)) + + then: 'Metric is persisted - analytics datasource is properly registered' + saved != null + saved.id != null + metricService.count() == 1 + } + + void "aggregate HQL routes to analytics datasource via data service"() { + given: 'Multiple metrics saved under tenant1' + metricService.save(new Metric(name: 'alpha', amount: 10)) + metricService.save(new Metric(name: 'beta', amount: 20)) + metricService.save(new Metric(name: 'gamma', amount: 30)) + + when: 'Running an aggregate query through the data service' + def results = metricService.getTotalAmountAbove(15) + + then: 'The HQL executes against the analytics datasource' + results.size() == 1 + results[0] == 50 // 20 + 30 + } + + private static void setTenant(String tenantId) { + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, tenantId) + } +} + +/** + * Metric domain mapped to the 'analytics' datasource with DISCRIMINATOR multi-tenancy. + * This combination triggers the allQualifiers() bug when both MultiTenant and + * a non-default datasource are configured. + */ +@Entity +class Metric implements GormEntity, MultiTenant { + Long id + Long version + String tenantId + String name + Integer amount + + static mapping = { + datasource 'analytics' + } + + static constraints = { + name blank: false + amount min: 0 + } +} + +/** + * Data Service interface for Metric - all methods auto-implemented by GORM. + */ +interface MetricDataService { + Metric get(Serializable id) + Metric save(Metric metric) + void delete(Serializable id) + Long count() + Metric findByName(String name) + List findAllByAmountGreaterThan(Integer amount) +} + +/** + * Abstract class that binds MetricDataService to the 'analytics' datasource. + * The @Transactional(connection = "analytics") ensures all auto-implemented methods + * and custom methods route to the secondary datasource. + */ +@Service(Metric) +@Transactional(connection = 'analytics') +abstract class MetricService implements MetricDataService { + + /** + * Delete all metrics for the current tenant from the analytics datasource. + * The @Transactional(connection = 'analytics') on this class ensures the + * executeUpdate routes to the analytics datasource. + */ + void deleteAll() { + Metric.executeUpdate('delete from Metric where 1=1') + } + + /** + * Aggregate query via domain class static API. + * Executes against analytics datasource via the active transaction. + */ + List getTotalAmountAbove(Integer minAmount) { + Metric.executeQuery( + 'select sum(m.amount) from Metric m where m.amount > :minAmount', + [minAmount: minAmount] + ) + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataSourceConnectionSourceFactorySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataSourceConnectionSourceFactorySpec.groovy new file mode 100644 index 00000000000..f0bf931e58a --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataSourceConnectionSourceFactorySpec.groovy @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.core.connections.ConnectionSource +import org.grails.datastore.gorm.jdbc.connections.DataSourceConnectionSourceFactory +import org.grails.datastore.gorm.jdbc.schema.DefaultSchemaHandler +import org.hibernate.dialect.Oracle8iDialect +import spock.lang.Specification + +/** + * Created by graemerocher on 05/07/16. + */ +class DataSourceConnectionSourceFactorySpec extends Specification { + + void "test datasource connection source factory"() { + when: + DataSourceConnectionSourceFactory factory = new DataSourceConnectionSourceFactory() + Map config = [ + 'dataSource.url':"jdbc:h2:mem:dsConnDsFactorySpecDb;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': Oracle8iDialect.name, + 'dataSource.properties.dbProperties': [useSSL: false] + ] + def connectionSource = factory.create(ConnectionSource.DEFAULT, DatastoreUtils.createPropertyResolver(config)) + + then:"The connection source is correct" + connectionSource.name == ConnectionSource.DEFAULT + connectionSource.source + + when:"The schema names are resolved" + def schemaNames = new DefaultSchemaHandler().resolveSchemaNames(connectionSource.source) + + then:"They are correct" + schemaNames == ['INFORMATION_SCHEMA', 'PUBLIC'] + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactorySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactorySpec.groovy new file mode 100644 index 00000000000..935ed5dd8aa --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactorySpec.groovy @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import grails.gorm.annotation.Entity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.core.connections.ConnectionSource +import org.grails.orm.hibernate.cfg.HibernateMappingContext +import org.hibernate.SessionFactory +import org.hibernate.dialect.H2Dialect +import org.hibernate.dialect.Oracle8iDialect +import spock.lang.Specification + +/** + * Created by graemerocher on 06/07/2016. + */ +class HibernateConnectionSourceFactorySpec extends Specification { + + void "Test hibernate connection factory"() { + when:"A factory is used to create a session factory" + + HibernateConnectionSourceFactory factory = new HibernateConnectionSourceFactory(Foo) + Map config = [ + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create' + ] + def connectionSource = factory.create(ConnectionSource.DEFAULT, DatastoreUtils.createPropertyResolver(config)) + + then:"The session factory is created" + connectionSource.source instanceof SessionFactory + connectionSource.source.getMetamodel().entity(Foo.name) + connectionSource.source.openSession().createCriteria(Foo).list().size() == 0 + + when:"The connection source is closed" + connectionSource.close() + + then:"The session factory is closed" + connectionSource.source.isClosed() + } +} + +@Entity +class Foo { + String name +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettingsSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettingsSpec.groovy new file mode 100644 index 00000000000..8f111e52f62 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettingsSpec.groovy @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import org.grails.datastore.mapping.core.DatastoreUtils +import org.hibernate.dialect.Oracle8iDialect +import org.springframework.core.io.FileSystemResource +import org.springframework.core.io.UrlResource +import spock.lang.Specification + +/** + * Created by graemerocher on 05/07/16. + */ +class HibernateConnectionSourceSettingsSpec extends Specification { + + void "test hibernate connection source settings"() { + when:"The configuration is built" + Map config = [ + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': Oracle8iDialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'commit', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create', + 'hibernate.cache':['region.factory_class':'org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory'], + 'hibernate.configLocations':'file:hibernate.cfg.xml', + 'org.hibernate.foo':'bar' + ] + HibernateConnectionSourceSettingsBuilder builder = new HibernateConnectionSourceSettingsBuilder(DatastoreUtils.createPropertyResolver(config)) + HibernateConnectionSourceSettings settings = builder.build() + + def expectedDataSourceProperties = new Properties() + expectedDataSourceProperties.put('hibernate.hbm2ddl.auto', 'update') + expectedDataSourceProperties.put('hibernate.show_sql', 'false') + expectedDataSourceProperties.put('hibernate.format_sql', 'true') + expectedDataSourceProperties.put('hibernate.dialect', Oracle8iDialect.name) + + def expectedHibernateProperties = new Properties() + expectedHibernateProperties.put('hibernate.hbm2ddl.auto', 'create') + expectedHibernateProperties.put('hibernate.cache.queries', 'true') + expectedHibernateProperties.put('hibernate.flush.mode', 'commit') + expectedHibernateProperties.put('hibernate.naming_strategy','org.hibernate.cfg.ImprovedNamingStrategy') + expectedHibernateProperties.put('hibernate.entity_dirtiness_strategy', 'org.grails.orm.hibernate.dirty.GrailsEntityDirtinessStrategy') + expectedHibernateProperties.put('hibernate.configLocations','file:hibernate.cfg.xml') + expectedHibernateProperties.put('hibernate.use_query_cache','true') + expectedHibernateProperties.put("hibernate.connection.handling_mode", "DELAYED_ACQUISITION_AND_HOLD") + expectedHibernateProperties.put('hibernate.cache.region.factory_class','org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory') + expectedHibernateProperties.put('org.hibernate.foo','bar') + + def expectedCombinedProperties = new Properties() + expectedCombinedProperties.putAll(expectedDataSourceProperties) + expectedCombinedProperties.putAll(expectedHibernateProperties) + + then:"The results are correct" + settings.dataSource.dbCreate == 'update' + settings.dataSource.dialect == Oracle8iDialect + settings.dataSource.formatSql + !settings.dataSource.logSql + settings.dataSource.toHibernateProperties() == expectedDataSourceProperties + + settings.hibernate.getFlush().mode == HibernateConnectionSourceSettings.HibernateSettings.FlushSettings.FlushMode.COMMIT + settings.hibernate.getCache().queries + settings.hibernate.get('hbm2ddl.auto') == 'create' + settings.hibernate.getConfigLocations().size() == 1 + settings.hibernate.getConfigLocations()[0] instanceof UrlResource + settings.hibernate.toProperties() == expectedHibernateProperties + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy new file mode 100644 index 00000000000..31b347288cb --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import grails.gorm.services.Service +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Transactional +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.Session +import org.hibernate.dialect.H2Dialect +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 06/07/2016. + */ +class MultipleDataSourceConnectionsSpec extends Specification { + @Shared Map config = [ + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'create-drop', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create-drop', + 'dataSources.books':[url:"jdbc:h2:mem:books;LOCK_TIMEOUT=10000"], + 'dataSources.moreBooks.url':"jdbc:h2:mem:moreBooks;LOCK_TIMEOUT=10000", + 'dataSources.moreBooks.hibernate.default_schema':"schema2" + ] + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config),Book, Author ) + + void "Test map to multiple data sources"() { + + when: "The default data source is used" + int result = Author.withTransaction { + new Author(name: 'Fred').save(flush:true) + Author.count() + } + + + + then:"The default data source is bound" + result ==1 + Book.withNewSession { Session s -> + assert s.connection().metaData.getURL() == "jdbc:h2:mem:books" + return true + } + Book.moreBooks.withNewSession { Session s -> + assert s.connection().metaData.getURL() == "jdbc:h2:mem:moreBooks" + return true + } + Author.withNewSession { Author.count() == 1 } + Author.withNewSession { Session s -> + assert s.connection().metaData.getURL() == "jdbc:h2:mem:grailsDB" + return true + } + Author.books.withNewSession { Session s -> + assert s.connection().metaData.getURL() == "jdbc:h2:mem:books" + return true + } + Author.moreBooks.withNewSession { Session s -> + assert s.connection().metaData.getURL() == "jdbc:h2:mem:moreBooks" + return true + } + + when:"A book is saved" + Book b = Book.withTransaction { + new Book(name: "The Stand").save(flush:true) + Book.first() + } + + + + then:"The data was saved correctly" + b.name == 'The Stand' + b.dateCreated + b.lastUpdated + + + when:"A new data source is added at runtime" + datastore.connectionSources.addConnectionSource("yetAnother", [pooled : true, + dbCreate : "create-drop", + logSql : false, + formatSql : true, + url : "jdbc:h2:mem:yetAnotherDB;LOCK_TIMEOUT=10000"]) + + then:"The other data sources have not been touched" + Author.withTransaction { Author.count() } == 1 + Book.withTransaction { Book.count() } == 1 + Author.yetAnother.withNewSession { Session s -> + assert s.connection().metaData.getURL() == "jdbc:h2:mem:yetAnotherDB" + return true + } + } + + void "static GORM operations use first non-default datasource for multi datasource entity"() { + given: "a unique book name" + def uniqueName = "The Stand ${UUID.randomUUID()}" + + when: "saving a book to the books datasource" + Book.withTransaction { + new Book(name: uniqueName).save(flush: true) + } + + then: "withNewSession uses books datasource" + Book.withNewSession { Session s -> + assert s.connection().metaData.getURL() == "jdbc:h2:mem:books" + return true + } + + when: "executing a static query" + def books = Book.withTransaction { + Book.executeQuery("from Book where name = :name", [name: uniqueName]) + } + + then: "the books datasource is queried" + books.size() == 1 + + when: "executing criteria query" + def criteriaResults = Book.withTransaction { + Book.withCriteria { + eq('name', uniqueName) + } + } + + then: "criteria uses the books datasource" + criteriaResults.size() == 1 + + when: "executing update" + def updatedName = "The Stand Updated ${UUID.randomUUID()}" + int updated = Book.withTransaction { + Book.executeUpdate("update Book set name = :name where name = :oldName", [name: updatedName, oldName: uniqueName]) + } + + then: "update affects the books datasource" + updated == 1 + Book.withTransaction { Book.findByName(updatedName) } != null + + when: "executing a static transaction" + int count = Book.withTransaction { + Book.countByName(updatedName) + } + + then: "transaction uses the books datasource" + count == 1 + } + + void "ALL mapped entity uses default datasource for withNewSession"() { + when: "requesting a new session for ALL mapped entity" + def url = Author.withNewSession { Session s -> + s.connection().metaData.getURL() + } + + then: "default datasource is used" + url == "jdbc:h2:mem:grailsDB" + } + + void "test @Transactional with connection property to non-default database"() { + when: + TestService testService = datastore.getDatastoreForConnection("books").getService(TestService) + then: + testService != null + } +} + +@Entity +class Book { + Long id + Long version + String name + Date dateCreated + Date lastUpdated + + static mapping = { + datasources( ['books', 'moreBooks'] ) + } + static constraints = { + name blank:false + } +} + +@Entity +class Author { + Long id + Long version + String name + + static mapping = { + datasource 'ALL' + } + static constraints = { + name blank:false + } +} + +@Service +@Transactional(connection = "books") +class TestService { +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceMetadataSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceMetadataSpec.groovy new file mode 100644 index 00000000000..9ab094f2072 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceMetadataSpec.groovy @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import grails.gorm.annotation.Entity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.boot.Metadata +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class MultipleDataSourceMetadataSpec extends Specification { + + @Shared + Map config = [ + "dataSources.apples.url": "jdbc:h2:mem:apples;LOCK_TIMEOUT=10000", + "dataSources.oranges.url": "jdbc:h2:mem:oranges;LOCK_TIMEOUT=10000" + ] + + @AutoCleanup + @Shared + HibernateDatastore datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), Apple, Orange) + + void "test metadata retrieval for multiple dataSources"() { + when: "the metadata for the default dataSource is retrieved" + Metadata metadataDefault = datastore.metadata + + then: "the metadata is set and does not contain entityBindings or tableMappings" + metadataDefault.entityBindings.size() == 0 + metadataDefault.collectTableMappings().size() == 0 + + when: "the metadata for the apples dataSource is retrieved" + Metadata metadataApples = datastore.getDatastoreForConnection("apples").metadata + + then: "the metadata is set and does contain the correct entityBinding and tableMapping" + metadataApples.entityBindings.size() == 1 + metadataApples.entityBindings.first().getMappedClass() == Apple + metadataApples.collectTableMappings().size() == 1 + metadataApples.collectTableMappings().first().name == "apple" + + when: "the metadata for the oranges dataSource is retrieved" + Metadata metadataOranges = datastore.getDatastoreForConnection("oranges").metadata + + then: "the metadata is set and does contain the correct entityBinding and tableMapping" + metadataOranges.entityBindings.size() == 1 + metadataOranges.entityBindings.first().getMappedClass() == Orange + metadataOranges.collectTableMappings().size() == 1 + metadataOranges.collectTableMappings().first().name == "orange" + } +} + +@Entity +class Apple { + + String name + + static mapping = { + datasource "apples" + } + +} + +@Entity +class Orange { + + Integer age + + static mapping = { + datasource "oranges" + } + +} + + diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithCachingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithCachingSpec.groovy new file mode 100644 index 00000000000..6c83cc0b502 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithCachingSpec.groovy @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import grails.gorm.annotation.Entity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.dialect.H2Dialect +import spock.lang.Specification + +/** + * Created by graemerocher on 15/07/2016. + */ +class MultipleDataSourcesWithCachingSpec extends Specification { + + void "Test map to multiple data sources"() { + given:"A configuration for multiple data sources" + Map config = [ + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.cache':['use_second_level_cache':true,'region.factory_class':'org.hibernate.cache.ehcache.EhCacheRegionFactory'], + 'hibernate.hbm2ddl.auto': 'create', + 'dataSources.books':[url:"jdbc:h2:mem:books;LOCK_TIMEOUT=10000"], + 'dataSources.moreBooks':[url:"jdbc:h2:mem:moreBooks;LOCK_TIMEOUT=10000"] + ] + + when: + HibernateDatastore datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config),CachingBook ) + CachingBook book = CachingBook.withTransaction { + new CachingBook(name:"The Stand").save(flush:true) + CachingBook.get( CachingBook.first().id ) + + } + + then: + book != null + + } +} +@Entity +class CachingBook { + Long id + Long version + String name + + static mapping = { + cache true + datasources( ['books', 'moreBooks'] ) + } + static constraints = { + name blank:false + } +} + + diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithEventsSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithEventsSpec.groovy new file mode 100644 index 00000000000..2ff65b7a658 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithEventsSpec.groovy @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import grails.gorm.annotation.Entity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.core.connections.ConnectionSource +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.dialect.H2Dialect +import spock.lang.Issue +import spock.lang.Specification + +/** + * Created by graemerocher on 20/02/2017. + */ +class MultipleDataSourcesWithEventsSpec extends Specification { + + @Issue('https://github.com/apache/grails-core/issues/10451') + void "Test multiple data sources register the correct events"() { + given:"A configuration for multiple data sources" + Map config = [ + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.cache':['use_second_level_cache':true,'region.factory_class':'org.hibernate.cache.ehcache.EhCacheRegionFactory'], + 'hibernate.hbm2ddl.auto': 'create', + 'dataSources.books':[url:"jdbc:h2:mem:books;LOCK_TIMEOUT=10000"] + ] + + when:"A entity is saved with the default connection" + HibernateDatastore datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config),EventsBook, SecondaryBook ) + EventsBook book = new EventsBook(name:"test") + EventsBook.withTransaction { + book.save(flush:true) + book.discard() + book = EventsBook.get(book.id) + } + + + + then:"The events were triggered" + book != null + book.name == 'TEST' + book.time.startsWith("Time: ") + + + when:"A entity is saved with a secondary connection connection" + EventsBook book2 = new EventsBook(name:"test2") + EventsBook.books.withTransaction { + book2.books.save(flush:true) + book2.books.discard() + book2 = EventsBook.books.get(book2.id) + } + + + + then:"The events were triggered" + book2 != null + book2.name == 'TEST2' + book2.time.startsWith("Time: ") + + when:"An entity is saved that uses only a secondary datasource" + SecondaryBook book3 = new SecondaryBook(name:"test3") + SecondaryBook.withTransaction { + book3.save(flush:true) + book3.discard() + book3 = SecondaryBook.get(book3.id) + } + + + + then:"The events were triggered" + book3 != null + book3.name == 'TEST3' + book3.time.startsWith("Time: ") + } +} + +@Entity +class SecondaryBook { + String time + String name + def beforeValidate() { + time = "Time: ${System.currentTimeMillis()}" + } + + def beforeInsert() { + name = name.toUpperCase() + } + + static mapping = { + datasource "books" + } +} + +@Entity +class EventsBook { + String time + String name + def beforeValidate() { + time = "Time: ${System.currentTimeMillis()}" + } + + def beforeInsert() { + name = name.toUpperCase() + } + + static mapping = { + datasource ConnectionSource.ALL + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/PartitionedMultiTenancySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/PartitionedMultiTenancySpec.groovy new file mode 100644 index 00000000000..fd364002fe4 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/PartitionedMultiTenancySpec.groovy @@ -0,0 +1,397 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import grails.gorm.DetachedCriteria +import grails.gorm.MultiTenant +import grails.gorm.hibernate.mapping.MappingBuilder +import grails.gorm.multitenancy.CurrentTenant +import grails.gorm.multitenancy.Tenant +import grails.gorm.multitenancy.Tenants +import grails.gorm.transactions.Rollback +import grails.gorm.annotation.Entity +import org.grails.datastore.gorm.GormEntity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.multitenancy.AllTenantsResolver +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings +import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException +import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.Session +import org.hibernate.dialect.H2Dialect +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by graemerocher on 11/07/2016. + */ +@Rollback +class PartitionedMultiTenancySpec extends Specification { + + @Shared @AutoCleanup HibernateDatastore datastore + void setupSpec() { + Map config = [ + "grails.gorm.multiTenancy.mode":MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR, + "grails.gorm.multiTenancy.tenantResolverClass":MyTenantResolver, + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'dataSource.logSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create', + ] + + datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), MultiTenantAuthor, MultiTenantBook, MultiTenantPublisher ) + } + + Session getSession() { datastore.sessionFactory.currentSession } + + void setup() { + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "") + } + + void cleanup() { + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "") + } + + + void "Test partitioned multi tenancy"() { + when:"no tenant id is present" + MultiTenantAuthor.list() + + + then:"An exception is thrown" + thrown(TenantNotFoundException) + + when:"no tenant id is present" + def author = new MultiTenantAuthor(name: "Stephen King") + author.save(flush:true) + + then:"An exception is thrown" + !author.errors.hasErrors() + thrown(TenantNotFoundException) + + when:"A tenant id is present" + datastore.sessionFactory.currentSession.clear() + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "moreBooks") + + then:"the correct tenant is used" + MultiTenantAuthor.count() == 0 + + when:"An object is saved" + author = new MultiTenantAuthor(name: "Stephen King") + author.save(flush: true) + + then:"The results are correct" + author.tmp != null // the beforeInsert event was triggered + MultiTenantAuthor.findByName("Stephen King") + MultiTenantAuthor.findAll("from MultiTenantAuthor a").size() == 1 + MultiTenantAuthor.count() == 1 + + when:"An a transaction is used" + MultiTenantAuthor.withTransaction{ + new MultiTenantAuthor(name: "JRR Tolkien").save(flush:true) + } + + then:"The results are correct" + MultiTenantAuthor.count() == 2 + + when:"The tenant id is switched" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "books") + + then:"the correct tenant is used" + MultiTenantAuthor.count() == 0 + !MultiTenantAuthor.findByName("Stephen King") + MultiTenantAuthor.findAll("from MultiTenantAuthor a").size() == 0 + MultiTenantAuthor.withTenant("moreBooks").count() == 2 + MultiTenantAuthor.withTenant("moreBooks") { String tenantId, Session s -> + assert s != null + MultiTenantAuthor.count() == 2 + } + Tenants.withId("books") { + MultiTenantAuthor.count() == 0 + new MultiTenantAuthor(name: "James Patterson").save(flush:true) + } + Tenants.withId("moreBooks") { + MultiTenantAuthor.count() == 2 + } + Tenants.withId("moreBooks") { + MultiTenantAuthor.withCriteria { + eq 'name', 'James Patterson' + }.size() == 0 + } + + + Tenants.withCurrent { + def results = MultiTenantAuthor.withCriteria { + eq 'name', 'James Patterson' + } + results.size() == 1 + } + Tenants.withCurrent { + MultiTenantAuthor.findByName('James Patterson') != null + } + Tenants.withCurrent { + MultiTenantAuthor.count() == 1 + } + + when:"each tenant is iterated over" + Map tenantIds = [:] + MultiTenantAuthor.eachTenant { String tenantId -> + tenantIds.put(tenantId, MultiTenantAuthor.count()) + } + + then:"The result is correct" + tenantIds == [moreBooks:2, books:1] + + when:"A tenant service is used" + MultiTenantAuthorService authorService = new MultiTenantAuthorService() + + then:"The service works correctly" + authorService.countAuthors() == 1 + authorService.countMoreAuthors() == 2 + + } + + void "test multi tenancy and associations"() { + when:"A tenant id is present" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "books") + + MultiTenantAuthor.withTransaction { + new MultiTenantAuthor(name: "Stephen King") + .addTo("books", [title:"The Stand"]) + .addTo("books", [title:"The Shining"]) + .save() + + new MultiTenantPublisher(name: "Fluff").save() + } + + session.clear() + MultiTenantAuthor author = MultiTenantAuthor.findByName("Stephen King") + MultiTenantPublisher publisher = MultiTenantPublisher.first() + + then:"The association ids are loaded with the tenant id" + author.name == "Stephen King" + author.books.size() == 2 + author.books.every() { MultiTenantBook book -> book.tenantCode == 'books'} + publisher.tenantCode == 'books' + + } + + void "Test first "() { + given: "Create two Authors with tenant T0" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + MultiTenantAuthor.saveAll([new MultiTenantAuthor(name: "A")]) + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'OTHER TENANT') + MultiTenantAuthor.saveAll([new MultiTenantAuthor(name: "B")]) + + when: "Query with no tenant" + datastore.sessionFactory.currentSession.clear() + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, '') + MultiTenantAuthor.first() + then: "An exception is thrown" + thrown(TenantNotFoundException) + + when: "Query with a TENANT" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + then: + MultiTenantAuthor.first().name == 'A' + + when: "Query with OTHER TENANT" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'OTHER TENANT') + then: + MultiTenantAuthor.first().name == 'B' + } + + + void "Test last "() { + given: "Create two Authors with tenant T0" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + MultiTenantAuthor.saveAll([new MultiTenantAuthor(name: "A")]) + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'OTHER TENANT') + MultiTenantAuthor.saveAll([new MultiTenantAuthor(name: "B")]) + + when: "Query with no tenant" + datastore.sessionFactory.currentSession.clear() + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, '') + MultiTenantAuthor.last() + then: "An exception is thrown" + thrown(TenantNotFoundException) + + when: "Query with a TENANT" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + then: + MultiTenantAuthor.last().name == 'A' + + when: "Query with OTHER TENANT" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'OTHER TENANT') + then: + MultiTenantAuthor.last().name == 'B' + } + + void "Test findAll with max params"() { + given: "Create two Authors with tenant T0" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + MultiTenantAuthor.saveAll([new MultiTenantAuthor(name: "A")]) + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'OTHER TENANT') + MultiTenantAuthor.saveAll([new MultiTenantAuthor(name: "B")]) + + when: "Query with no tenant" + datastore.sessionFactory.currentSession.clear() + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, '') + MultiTenantAuthor.findAll([max:2]) + then: "An exception is thrown" + thrown(TenantNotFoundException) + + when: "Query with a TENANT" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + then: + MultiTenantAuthor.findAll([max:2]).name == ['A'] + + when: "Query with OTHER TENANT" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'OTHER TENANT') + then: + MultiTenantAuthor.findAll([max:2]).name == ['B'] + } + + void "Test list without 'max' parameter"() { + given: "Create two Authors with tenant T0" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + MultiTenantAuthor.saveAll([new MultiTenantAuthor(name: "A"), new MultiTenantAuthor(name: "B")]) + + when: "Query with no tenant" + datastore.sessionFactory.currentSession.clear() + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, '') + MultiTenantAuthor.list() + then: "An exception is thrown" + thrown(TenantNotFoundException) + + when: "Query with the same tenant as saved, should obtain 2 entities" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + then: + MultiTenantAuthor.list().size() == 2 + } + + void "Test list with 'max' parameter"() { + given: "Create two Authors with tenant T0" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + MultiTenantAuthor.saveAll([new MultiTenantAuthor(name: "A"), new MultiTenantAuthor(name: "B")]) + + when: "Query with no tenant" + datastore.sessionFactory.currentSession.clear() + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, '') + MultiTenantAuthor.list([max: 2]) + then: "An exception is thrown" + thrown(TenantNotFoundException) + + when: "Query with the same tenant as saved, should obtain 2 entities" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'TENANT') + then: + MultiTenantAuthor.list().size() == 2 + + when: "Check the paged results" + def sameTenantList = MultiTenantAuthor.list([max:1]) + then: + sameTenantList.size() == 1 + sameTenantList.getTotalCount() == 2 + + when: "Query by another tenant, should obtain no entities" + datastore.sessionFactory.currentSession.clear() + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'OTHER TENANT') + def list = MultiTenantAuthor.list([max: 2]) + then: + list.size() == 0 + list.getTotalCount() == 0 + } +} + +class MyTenantResolver extends SystemPropertyTenantResolver implements AllTenantsResolver { + + Iterable resolveTenantIds() { + Tenants.withoutId { + def tenantIds = new DetachedCriteria(MultiTenantAuthor) + .distinct('tenantId') + .list() + return tenantIds + } + } + +} +@Entity +class MultiTenantAuthor implements GormEntity,MultiTenant { + Long id + Long version + String tenantId + String name + transient String tmp + + def beforeInsert() { + tmp = "foo" + } + static hasMany = [books:MultiTenantBook] + static constraints = { + name blank:false + } +} + +@CurrentTenant +class MultiTenantAuthorService { + int countAuthors() { + MultiTenantAuthor.count() + } + + @Tenant({ "moreBooks" }) + int countMoreAuthors() { + MultiTenantAuthor.count() + } +} + +@Entity +class MultiTenantBook implements GormEntity,MultiTenant { + Long id + Long version + String tenantCode + String title + + + + static belongsTo = [author:MultiTenantAuthor] + static constraints = { + title blank:false + } + + static mapping = { + tenantId name:"tenantCode" + } +} + + +@Entity +class MultiTenantPublisher implements GormEntity,MultiTenant { + String tenantCode + String name + + static mapping = MappingBuilder.orm { + tenantId "tenantCode" + } +} + diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SchemaMultiTenantSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SchemaMultiTenantSpec.groovy new file mode 100644 index 00000000000..599181c7fa9 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SchemaMultiTenantSpec.groovy @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import grails.gorm.multitenancy.Tenants +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.multitenancy.AllTenantsResolver +import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException +import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.Session +import org.hibernate.dialect.H2Dialect +import org.hibernate.resource.jdbc.spi.JdbcSessionOwner +import org.grails.orm.hibernate.support.hibernate5.SessionHolder +import org.springframework.transaction.support.TransactionSynchronizationManager +import spock.lang.AutoCleanup +import spock.util.environment.RestoreSystemProperties +import spock.lang.Shared +import spock.lang.Specification + +import java.sql.Connection + +/** + * Created by graemerocher on 20/07/2016. + */ +@RestoreSystemProperties +class SchemaMultiTenantSpec extends Specification { + + void "Test a database per tenant multi tenancy"() { + given:"A configuration for multiple data sources" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "") + Map config = [ + "grails.gorm.multiTenancy.mode":"SCHEMA", + "grails.gorm.multiTenancy.tenantResolverClass":MyResolver, + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create', + ] + + HibernateDatastore datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), SingleTenantAuthor ) + HibernateConnectionSource connectionSource = datastore.getConnectionSources().defaultConnectionSource + def connection = connectionSource.dataSource.getConnection() + connection.close() + when:"no tenant id is present" + SingleTenantAuthor.list() + + then:"An exception is thrown" + thrown(TenantNotFoundException) + + when:"no tenant id is present" + new SingleTenantAuthor().save() + + then:"An exception is thrown" + thrown(TenantNotFoundException) + + when:"A tenant id is present" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "moreBooks") + + then:"the correct tenant is used" + SingleTenantAuthor.withTransaction { SingleTenantAuthor.count() == 0 } + SingleTenantAuthor.withTransaction { + SingleTenantAuthor.withSession { Session s -> + def conn = ((JdbcSessionOwner) s).getJdbcConnectionAccess().obtainConnection() + assert conn.metaData.getURL() == "jdbc:h2:mem:grailsDB" + return true + } + } + + when:"An object is saved" + SingleTenantAuthor.withTransaction { + SingleTenantAuthor.withSession{ Session s -> + def conn = ((JdbcSessionOwner) s).getJdbcConnectionAccess().obtainConnection() + assert conn.metaData.getURL() == "jdbc:h2:mem:grailsDB" + new SingleTenantAuthor(name: "Stephen King").save(flush:true) + } + } + + then:"The results are correct" + SingleTenantAuthor.withTransaction { SingleTenantAuthor.count() == 1 } + + when: + def author = SingleTenantAuthor.withNewSession { + SingleTenantAuthor.find { name == "Stephen King"} + } + + then: + author != null + + when:"An a transaction is used" + SingleTenantAuthor.withTransaction{ + new SingleTenantAuthor(name: "James Patterson").save(flush:true) + } + + then:"The results are correct" + SingleTenantAuthor.withTransaction { SingleTenantAuthor.count() == 2 } + + when:"The tenant id is switched" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "books") + + + then:"the correct tenant is used" + SingleTenantAuthor.withTransaction { SingleTenantAuthor.count() == 0 } + SingleTenantAuthor.withTransaction { + + SingleTenantAuthor.withSession { Session s -> + def conn = ((JdbcSessionOwner) s).getJdbcConnectionAccess().obtainConnection() + assert conn.metaData.getURL() == "jdbc:h2:mem:grailsDB" + SingleTenantAuthor.count() == 0 + } + } + SingleTenantAuthor.withTenant("moreBooks") { String tenantId, Session s -> + assert s != null + SingleTenantAuthor.count() == 2 + } + Tenants.withId("books") { + SingleTenantAuthor.count() == 0 + } + Tenants.withId("moreBooks") { + SingleTenantAuthor.count() == 2 + } + Tenants.withCurrent { + SingleTenantAuthor.count() == 0 + } + + SingleTenantAuthor.withTransaction{ + new SingleTenantAuthor(name: "JRR Tolkien").save(flush:true) + } + + when:"A new tenant is added at runtime" + MyResolver.tenantIds.add("evenMoreBooks") + datastore.addTenantForSchema("evenMoreBooks") + + then: + SingleTenantAuthor.withTenant("evenMoreBooks") { String tenantId, Session s -> + assert s != null + def count = SingleTenantAuthor.count() + count == 0 + } + + when:"each tenant is iterated over" + Map tenantIds = [:] + SingleTenantAuthor.eachTenant { String tenantId -> + tenantIds.put(tenantId, SingleTenantAuthor.count()) + } + + then:"The result is correct" + tenantIds == [moreBooks:2, books:1, evenMoreBooks:0] + + + } + + static class MyResolver extends SystemPropertyTenantResolver implements AllTenantsResolver { + static List tenantIds = ["moreBooks", "books"] + @Override + Iterable resolveTenantIds() { + + return tenantIds + } + } + +} + diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SecondLevelCacheSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SecondLevelCacheSpec.groovy new file mode 100644 index 00000000000..cb7f836a5e4 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SecondLevelCacheSpec.groovy @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import grails.gorm.annotation.Entity +import groovy.transform.EqualsAndHashCode +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.dialect.H2Dialect +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class SecondLevelCacheSpec extends Specification { + @Shared @AutoCleanup HibernateDatastore datastore + void setupSpec() { + Map config = [ + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'dataSource.logSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.cache': ['use_second_level_cache': true, 'region.factory_class': 'org.hibernate.cache.ehcache.EhCacheRegionFactory'], + 'hibernate.hbm2ddl.auto': 'create', + ] + + datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), CachingEntity) + } + + void "Test second level cache"() { + when: + datastore.sessionFactory.getStatistics().setStatisticsEnabled(true) + def id = CachingEntity.withNewTransaction { + new CachingEntity(name: 'test').save() + CachingEntity.first().id + } + + String[] regionNames = datastore.sessionFactory.getStatistics().getSecondLevelCacheRegionNames() + + then: + regionNames.size() == 1 + datastore.sessionFactory.getStatistics().getCacheRegionStatistics(regionNames[0]).getMissCount() == 0 + datastore.sessionFactory.getStatistics().getCacheRegionStatistics(regionNames[0]).getHitCount() == 0 + + when: + CachingEntity entity1 = CachingEntity.withNewTransaction { + CachingEntity.get(id) + } + + then: + datastore.sessionFactory.getStatistics().getCacheRegionStatistics(regionNames[0]).getMissCount() == 1 + datastore.sessionFactory.getStatistics().getCacheRegionStatistics(regionNames[0]).getHitCount() == 0 + entity1 != null + + when: + CachingEntity entity2 = CachingEntity.withNewTransaction { + CachingEntity.get(id) + } + + then: + datastore.sessionFactory.getStatistics().getCacheRegionStatistics(regionNames[0]).getMissCount() == 1 + datastore.sessionFactory.getStatistics().getCacheRegionStatistics(regionNames[0]).getHitCount() == 1 + entity1 == entity2 + } +} + +@Entity +@EqualsAndHashCode +class CachingEntity { + String name + + static mapping = { + cache true + } + + static constraints = { + name blank: false + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SingleTenantSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SingleTenantSpec.groovy new file mode 100644 index 00000000000..5c32e23af2a --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SingleTenantSpec.groovy @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import grails.gorm.MultiTenant +import grails.gorm.multitenancy.CurrentTenant +import grails.gorm.multitenancy.Tenant +import grails.gorm.multitenancy.Tenants +import grails.gorm.annotation.Entity +import org.grails.datastore.gorm.GormEntity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException +import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver +import org.grails.orm.hibernate.HibernateDatastore +import org.hibernate.Session +import org.hibernate.dialect.H2Dialect +import org.hibernate.resource.jdbc.spi.JdbcSessionOwner +import spock.util.environment.RestoreSystemProperties +import spock.lang.Specification + +import java.sql.Connection + +/** + * Created by graemerocher on 07/07/2016. + */ +@RestoreSystemProperties +class SingleTenantSpec extends Specification { + + void "Test a database per tenant multi tenancy"() { + given:"A configuration for multiple data sources" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "") + Map config = [ + "grails.gorm.multiTenancy.mode":"DATABASE", + "grails.gorm.multiTenancy.tenantResolverClass":SystemPropertyTenantResolver, + 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'update', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create', + 'dataSources.books':[url:"jdbc:h2:mem:books;LOCK_TIMEOUT=10000"], + 'dataSources.moreBooks':[url:"jdbc:h2:mem:moreBooks;LOCK_TIMEOUT=10000"] + ] + + HibernateDatastore datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config),Book, SingleTenantAuthor ) + + when:"no tenant id is present" + SingleTenantAuthor.list() + + then:"An exception is thrown" + thrown(TenantNotFoundException) + + when:"no tenant id is present" + new SingleTenantAuthor().save() + + then:"An exception is thrown" + thrown(TenantNotFoundException) + + when:"A tenant id is present" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "moreBooks") + + then:"the correct tenant is used" + SingleTenantAuthor.withTransaction { SingleTenantAuthor.count() == 0 } + SingleTenantAuthor.withTransaction { + SingleTenantAuthor.withSession { Session s -> + def connection = ((JdbcSessionOwner) s).getJdbcConnectionAccess().obtainConnection() + assert connection.metaData.getURL() == "jdbc:h2:mem:moreBooks" + return true + } + } + + when:"An object is saved" + SingleTenantAuthor.withTransaction { + SingleTenantAuthor.withSession { Session s -> + def connection = ((JdbcSessionOwner) s).getJdbcConnectionAccess().obtainConnection() + assert connection.metaData.getURL() == "jdbc:h2:mem:moreBooks" + new SingleTenantAuthor(name: "Stephen King").save(flush:true) + } + } + + then:"The results are correct" + SingleTenantAuthor.withTransaction { SingleTenantAuthor.count() == 1 } + + when:"An a transaction is used" + SingleTenantAuthor.withTransaction{ + new SingleTenantAuthor(name: "James Patterson").save(flush:true) + } + + then:"The results are correct" + SingleTenantAuthor.withTransaction { SingleTenantAuthor.count() == 2 } + + when:"The tenant id is switched" + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "books") + + then:"the correct tenant is used" + SingleTenantAuthor.withTransaction { SingleTenantAuthor.count() == 0 } + SingleTenantAuthor.withTransaction { + SingleTenantAuthor.withSession { Session s -> + def connection = ((JdbcSessionOwner) s).getJdbcConnectionAccess().obtainConnection() + assert connection.metaData.getURL() == "jdbc:h2:mem:books" + SingleTenantAuthor.count() == 0 + } + } + SingleTenantAuthor.withTenant("moreBooks") { String tenantId, Session s -> + assert s != null + SingleTenantAuthor.count() == 2 + } + Tenants.withId("books") { + SingleTenantAuthor.count() == 0 + } + Tenants.withId("moreBooks") { + SingleTenantAuthor.count() == 2 + } + Tenants.withCurrent { + SingleTenantAuthor.count() == 0 + } + + when:"each tenant is iterated over" + Map tenantIds = [:] + SingleTenantAuthor.eachTenant { String tenantId -> + tenantIds.put(tenantId, SingleTenantAuthor.count()) + } + + then:"The result is correct" + tenantIds == [moreBooks:2, books:0] + + when:"A tenant service is used" + SingleTenantAuthorService authorService = new SingleTenantAuthorService() + + then:"The service works correctly" + authorService.countAuthors() == 0 + authorService.countMoreAuthors() == 2 + } + + +} + + +@Entity +class SingleTenantAuthor implements GormEntity,MultiTenant { + Long id + Long version + String name + + static constraints = { + name blank:false + } +} + +@CurrentTenant +class SingleTenantAuthorService { + int countAuthors() { + SingleTenantAuthor.count() + } + + @Tenant({ "moreBooks" }) + int countMoreAuthors() { + SingleTenantAuthor.count() + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy new file mode 100644 index 00000000000..d4a77aed768 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.connections + +import org.hibernate.dialect.H2Dialect +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +import grails.gorm.annotation.Entity +import grails.gorm.services.Service +import grails.gorm.services.Where +import grails.gorm.transactions.Transactional +import org.grails.datastore.gorm.GormEntity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.HibernateDatastore + +@Issue("https://github.com/apache/grails-core/issues/15416") +class WhereQueryMultiDataSourceSpec extends Specification { + + @Shared Map config = [ + 'dataSource.url':"jdbc:h2:mem:defaultDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'create-drop', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create-drop', + 'dataSources.secondary':[url:"jdbc:h2:mem:secondaryDB;LOCK_TIMEOUT=10000"], + ] + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore( + DatastoreUtils.createPropertyResolver(config), Item + ) + + @Shared ItemQueryService itemQueryService + + void setupSpec() { + itemQueryService = datastore + .getDatastoreForConnection('secondary') + .getService(ItemQueryService) + } + + void cleanup() { + Item.secondary.withNewTransaction { + Item.secondary.deleteAll(Item.secondary.list()) + } + Item.withNewTransaction { + Item.deleteAll(Item.list()) + } + } + + void "@Where query routes to secondary datasource"() { + given: + saveToSecondary('Cheap', 10.0) + saveToSecondary('Expensive', 500.0) + + when: + def results = itemQueryService.findByMinAmount(100.0) + + then: + results.size() == 1 + results[0].name == 'Expensive' + } + + void "@Where query does not return data from default datasource"() { + given: 'an item saved to secondary' + saveToSecondary('OnSecondary', 50.0) + + and: 'a different item saved directly to default' + saveToDefault('OnDefault', 999.0) + + when: 'querying via @Where for amount >= 500 on secondary-bound service' + def results = itemQueryService.findByMinAmount(500.0) + + then: 'only secondary data is searched - default item is NOT found' + results.size() == 0 + } + + void "count routes to secondary datasource"() { + given: + saveToSecondary('A', 1.0) + saveToSecondary('B', 2.0) + + and: 'an item on default that should not be counted' + saveToDefault('C', 3.0) + + expect: + itemQueryService.count() == 2 + } + + void "list routes to secondary datasource"() { + given: + saveToSecondary('X', 10.0) + saveToSecondary('Y', 20.0) + + and: 'an item on default that should not be listed' + saveToDefault('Z', 30.0) + + when: + def all = itemQueryService.list() + + then: + all.size() == 2 + } + + void "findByName routes to secondary datasource"() { + given: + saveToSecondary('Unique', 77.0) + + when: + def found = itemQueryService.findByName('Unique') + + then: + found != null + found.name == 'Unique' + found.amount == 77.0 + } + + private void saveToSecondary(String name, Double amount) { + Item.secondary.withNewTransaction { + new Item(name: name, amount: amount).secondary.save(flush: true) + } + } + + private void saveToDefault(String name, Double amount) { + Item.withNewTransaction { + new Item(name: name, amount: amount).save(flush: true) + } + } +} + +@Entity +class Item implements GormEntity { + Long id + Long version + String name + Double amount + + static mapping = { + datasource 'ALL' + } + + static constraints = { + name blank: false + amount nullable: false + } +} + +@Service(Item) +@Transactional(connection = 'secondary') +interface ItemQueryService { + + Item findByName(String name) + + Number count() + + List list() + + @Where({ amount >= minAmount }) + List findByMinAmount(Double minAmount) +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandlerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandlerSpec.groovy new file mode 100644 index 00000000000..2795bc9b26f --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandlerSpec.groovy @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.proxy + +import org.hibernate.collection.spi.PersistentCollection +import org.hibernate.proxy.HibernateProxy +import org.hibernate.proxy.LazyInitializer +import spock.lang.Specification + +class SimpleHibernateProxyHandlerSpec extends Specification { + + void "test isInitialized respects PersistentCollections"() { + given: + def ph = new HibernateProxyHandler() + + when: + def initialized = Mock(PersistentCollection) { + 1 * wasInitialized() >> true + } + def notInitialized = Mock(PersistentCollection) { + 1 * wasInitialized() >> false + } + + then: + ph.isInitialized(initialized) + !ph.isInitialized(notInitialized) + } + + void "test isInitialized respects HibernateProxy"() { + given: + def ph = new HibernateProxyHandler() + + when: + def initialized = Mock(HibernateProxy) { + 1 * getHibernateLazyInitializer() >> Mock(LazyInitializer) { + 1 * isUninitialized() >> false + } + } + def notInitialized = Mock(HibernateProxy) { + 1 * getHibernateLazyInitializer() >> Mock(LazyInitializer) { + 1 * isUninitialized() >> true + } + } + + then: + ph.isInitialized(initialized) + !ph.isInitialized(notInitialized) + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/HibernateVersionSupportSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/HibernateVersionSupportSpec.groovy new file mode 100644 index 00000000000..01ae884ce15 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/HibernateVersionSupportSpec.groovy @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.support + +import spock.lang.Specification + +/** + * Created by graemerocher on 04/04/2017. + */ +class HibernateVersionSupportSpec extends Specification { + + void 'test hibernate version is at least'() { + expect: + !HibernateVersionSupport.isAtLeastVersion("6.0.0") + HibernateVersionSupport.isAtLeastVersion("5.3.0") + } +} diff --git a/grails-data-hibernate7/core/src/test/resources/META-INF/services/org.apache.grails.data.testing.tck.base.GrailsDataTckManager b/grails-data-hibernate7/core/src/test/resources/META-INF/services/org.apache.grails.data.testing.tck.base.GrailsDataTckManager new file mode 100644 index 00000000000..ce171b3e54d --- /dev/null +++ b/grails-data-hibernate7/core/src/test/resources/META-INF/services/org.apache.grails.data.testing.tck.base.GrailsDataTckManager @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +org.apache.grails.data.hibernate7.core.GrailsDataHibernate7TckManager \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/resources/simplelogger.properties b/grails-data-hibernate7/core/src/test/resources/simplelogger.properties new file mode 100644 index 00000000000..2f5ac2062a5 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/resources/simplelogger.properties @@ -0,0 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +#org.slf4j.simpleLogger.defaultLogLevel=debug +#org.slf4j.simpleLogger.log.org.hibernate=trace +#org.slf4j.simpleLogger.log.org.hibernate.SQL=debug \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/build.gradle b/grails-data-hibernate7/dbmigration/build.gradle new file mode 100644 index 00000000000..8a9262c79d2 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/build.gradle @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +plugins { + id 'org.apache.grails.buildsrc.properties' + id 'org.apache.grails.buildsrc.dependency-validator' + id 'org.apache.grails.gradle.grails-plugin' + id 'org.apache.grails.buildsrc.compile' + id 'org.apache.grails.buildsrc.publish' + id 'org.apache.grails.buildsrc.sbom' + id 'org.apache.grails.gradle.grails-code-style' +} + +version = projectVersion +group = 'org.apache.grails' + +ext { + gormApiDocs = true + pomTitle = 'Grails Database Migration Plugin for Hibernate 7' + pomDescription = 'The Database Migration plugin helps you manage database changes, via Liquibase, while developing Grails applications for Hibernate 7' +} + +dependencies { + // TODO: Clarify and clean up dependencies + implementation platform(project(':grails-hibernate7-bom')) + + implementation("org.liquibase:liquibase-core") { + exclude group: 'javax.xml.bind', module: 'jaxb-api' + } + implementation("org.liquibase.ext:liquibase-hibernate5") { + exclude group: 'org.hibernate', module: 'hibernate-core' + exclude group: 'org.hibernate', module: 'hibernate-entitymanager' + exclude group: 'org.hibernate', module: 'hibernate-envers' + exclude group: 'com.h2database', module: 'h2' + exclude group: 'org.liquibase', module: 'liquibase-commercial' + exclude group: 'org.liquibase', module: 'liquibase-core' + } + + implementation(project(':grails-shell-cli')) { + exclude group: 'org.slf4j', module: 'slf4j-simple' + + // TODO: the shell cli is exporting groovy 3, while this project is expected to use groovy 4 + // this plugin needs split into commands & the plugin itself so that different versions + // of groovy can be used + exclude group: 'org.codehaus.groovy' + } + + compileOnly 'org.springframework.boot:spring-boot-starter-logging' + compileOnly 'org.springframework.boot:spring-boot-autoconfigure' + compileOnly project(':grails-data-hibernate7') + compileOnly project(':grails-core') + compileOnly 'org.apache.groovy:groovy-sql' + compileOnly 'org.apache.groovy:groovy-xml' + + testImplementation 'org.springframework.boot:spring-boot-starter-tomcat' + testImplementation project(':grails-data-hibernate7') + testImplementation project(':grails-core') + testImplementation project(':grails-testing-support-datamapping') + testImplementation project(':grails-testing-support-web') + testImplementation 'com.h2database:h2' +} + +tasks.named('jar', Jar) { + exclude('testapp/**/**') +} + +apply { + from rootProject.layout.projectDirectory.file('gradle/hibernate7-test-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle') +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommand.groovy new file mode 100644 index 00000000000..25f30e22876 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommand.groovy @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmChangelogSyncCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Mark all changes as executed in the database' + + void handle() { + withLiquibase { Liquibase liquibase -> + liquibase.changeLogSync(contexts) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmChangelogSyncSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmChangelogSyncSqlCommand.groovy new file mode 100644 index 00000000000..476d383410b --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmChangelogSyncSqlCommand.groovy @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmChangelogSyncSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Writes the SQL that will mark all changes as executed in the database to STDOUT or a file' + + void handle() { + def filename = args[0] + + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.changeLogSync(contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmClearChecksumsCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmClearChecksumsCommand.groovy new file mode 100644 index 00000000000..0006e1649b4 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmClearChecksumsCommand.groovy @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmClearChecksumsCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Removes current checksums from database. On next run checksums will be recomputed' + + void handle() { + withLiquibase { Liquibase liquibase -> + liquibase.clearCheckSums() + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDbDocCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDbDocCommand.groovy new file mode 100644 index 00000000000..059ff5208eb --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDbDocCommand.groovy @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmDbDocCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Generates Javadoc-like documentation based on current database and change log' + + void handle() { + def destination = args[0] ?: config.getProperty((String) "${configPrefix}.dbDocLocation", String) ?: 'build/dbdoc' + withLiquibase { Liquibase liquibase -> + liquibase.generateDocumentation(destination, contexts) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDiffCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDiffCommand.groovy new file mode 100644 index 00000000000..9e3f1e6a13c --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDiffCommand.groovy @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.database.Database + +import grails.dev.commands.ApplicationCommand +import grails.util.Environment +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmDiffCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Compares two databases and creates a changelog that will make the changes required to bring them into sync' + + void handle() { + def otherEnv = args[0] + if (!otherEnv) { + throw new DatabaseMigrationException('You must specify the environment to diff against') + } + if (Environment.getEnvironment(otherEnv) == Environment.current || otherEnv == Environment.current.name) { + throw new DatabaseMigrationException('You must specify a different environment than the one the command is running in') + } + + def filename = args[1] + + def outputChangeLogFile = resolveChangeLogFile(filename) + if (outputChangeLogFile) { + if (outputChangeLogFile.exists()) { + if (hasOption('force')) { + outputChangeLogFile.delete() + } else { + throw new DatabaseMigrationException("ChangeLogFile ${outputChangeLogFile} already exists!") + } + } + if (!outputChangeLogFile.parentFile.exists()) { + outputChangeLogFile.parentFile.mkdirs() + } + } + + withDatabase { Database referenceDatabase -> + withDatabase(getDataSourceConfig(getEnvironmentConfig(otherEnv))) { Database targetDatabase -> + doDiffToChangeLog(outputChangeLogFile, referenceDatabase, targetDatabase) + } + } + + if (outputChangeLogFile && hasOption('add')) { + appendToChangeLog(changeLogFile, outputChangeLogFile) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDropAllCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDropAllCommand.groovy new file mode 100644 index 00000000000..efbc21c533c --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmDropAllCommand.groovy @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.CatalogAndSchema +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmDropAllCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Drops all database objects owned by the user' + + void handle() { + def schemaNames = args[0] + def schemas = schemaNames?.split(',')?.collect { String schemaName -> new CatalogAndSchema(null, schemaName) } + + withLiquibase { Liquibase liquibase -> + if (schemas) { + liquibase.dropAll(schemas as CatalogAndSchema[]) + } else { + liquibase.dropAll() + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmFutureRollbackCountSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmFutureRollbackCountSqlCommand.groovy new file mode 100644 index 00000000000..899a550d927 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmFutureRollbackCountSqlCommand.groovy @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmFutureRollbackCountSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Writes SQL to roll back the database to the current state after changes in the changeslog have been applied' + + @Override + void handle() { + def number = args[0] + if (!number) { + throw new DatabaseMigrationException("The $name command requires a change set number argument") + } + if (!number.isNumber()) { + throw new DatabaseMigrationException("The change set number argument '$number' isn't a number") + } + + def filename = args[1] + + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.futureRollbackSQL(number.toInteger(), contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmFutureRollbackSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmFutureRollbackSqlCommand.groovy new file mode 100644 index 00000000000..2e7e0d66e08 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmFutureRollbackSqlCommand.groovy @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmFutureRollbackSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Writes SQL to roll back the database to the current state after the changes in the changeslog have been applied' + + @Override + void handle() { + def filename = args[0] + + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.futureRollbackSQL(contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGenerateChangelogCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGenerateChangelogCommand.groovy new file mode 100644 index 00000000000..73d1e226bd7 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGenerateChangelogCommand.groovy @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.database.Database + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmGenerateChangelogCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Generates an initial changelog XML or Groovy DSL file from the database' + + void handle() { + def filename = args[0] + + def outputChangeLogFile = resolveChangeLogFile(filename) + if (outputChangeLogFile) { + if (outputChangeLogFile.exists()) { + if (hasOption('force')) { + outputChangeLogFile.delete() + } else { + throw new DatabaseMigrationException("ChangeLogFile ${outputChangeLogFile} already exists!") + } + } + if (!outputChangeLogFile.parentFile.exists()) { + outputChangeLogFile.parentFile.mkdirs() + } + } + + withDatabase { Database database -> + doGenerateChangeLog(outputChangeLogFile, database) + } + + if (outputChangeLogFile && hasOption('add')) { + appendToChangeLog(changeLogFile, outputChangeLogFile) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGenerateGormChangelogCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGenerateGormChangelogCommand.groovy new file mode 100644 index 00000000000..8519c83732f --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGenerateGormChangelogCommand.groovy @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.database.Database + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmGenerateGormChangelogCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Generates an initial changelog XML or Groovy DSL file from current GORM classes' + + @Override + void handle() { + def filename = args[0] + + def outputChangeLogFile = resolveChangeLogFile(filename) + if (outputChangeLogFile) { + if (outputChangeLogFile.exists()) { + if (hasOption('force')) { + outputChangeLogFile.delete() + } else { + throw new DatabaseMigrationException("ChangeLogFile ${outputChangeLogFile} already exists!") + } + } + if (!outputChangeLogFile.parentFile.exists()) { + outputChangeLogFile.parentFile.mkdirs() + } + } + + withGormDatabase(applicationContext, dataSource) { Database database -> + doGenerateChangeLog(outputChangeLogFile, database) + } + + if (outputChangeLogFile && hasOption('add')) { + appendToChangeLog(changeLogFile, outputChangeLogFile) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGormDiffCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGormDiffCommand.groovy new file mode 100644 index 00000000000..52d68b92e0b --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmGormDiffCommand.groovy @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.database.Database + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmGormDiffCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Diffs GORM classes against a database and generates a changelog XML or Groovy DSL file' + + @Override + void handle() { + def filename = args[0] + def outputChangeLogFile = resolveChangeLogFile(filename) + if (outputChangeLogFile) { + if (outputChangeLogFile.exists()) { + if (hasOption('force')) { + outputChangeLogFile.delete() + } else { + throw new DatabaseMigrationException("ChangeLogFile ${outputChangeLogFile} already exists!") + } + } + if (!outputChangeLogFile.parentFile.exists()) { + outputChangeLogFile.parentFile.mkdirs() + } + } + + withGormDatabase(applicationContext, dataSource) { Database referenceDatabase -> + withDatabase { Database targetDatabase -> + doDiffToChangeLog(outputChangeLogFile, referenceDatabase, targetDatabase) + } + } + + if (outputChangeLogFile && hasOption('add')) { + appendToChangeLog(changeLogFile, outputChangeLogFile) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmListLocksCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmListLocksCommand.groovy new file mode 100644 index 00000000000..c53d1c9af70 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmListLocksCommand.groovy @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmListLocksCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Lists who currently has locks on the database changelog to STDOUT or a file' + + void handle() { + def filename = args[0] + + withLiquibase { Liquibase liquibase -> + withFilePrintStreamOrSystemOut(filename) { PrintStream printStream -> + liquibase.reportLocks(printStream) + } + } + } + + private static void withFilePrintStreamOrSystemOut(String filename, @ClosureParams(value = SimpleType, options = 'java.io.PrintStream') Closure closure) { + if (!filename) { + closure.call(System.out) + return + } + + def outputFile = new File(filename) + if (!outputFile.parentFile.exists()) { + outputFile.parentFile.mkdirs() + } + outputFile.withOutputStream { OutputStream out -> + closure.call(new PrintStream(out)) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanCommand.groovy new file mode 100644 index 00000000000..4ba6e019872 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanCommand.groovy @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmMarkNextChangesetRanCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Mark the next change set as executed in the database' + + void handle() { + withLiquibase { Liquibase liquibase -> + liquibase.markNextChangeSetRan(contexts) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanSqlCommand.groovy new file mode 100644 index 00000000000..427c424b942 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanSqlCommand.groovy @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmMarkNextChangesetRanSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Writes SQL to mark the next change as executed in the database to STDOUT or a file' + + void handle() { + def filename = args[0] + + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.markNextChangeSetRan(contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmPreviousChangesetSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmPreviousChangesetSqlCommand.groovy new file mode 100644 index 00000000000..0d4e77405f7 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmPreviousChangesetSqlCommand.groovy @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase +import liquibase.database.Database + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmPreviousChangesetSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Generates the SQL to apply the previous change sets' + + @Override + void handle() { + + String count = args[0] + if (!count) { + throw new DatabaseMigrationException("The $name command requires a change set number argument") + } + if (!count.isNumber()) { + throw new DatabaseMigrationException("The change set number argument '$count' isn't a number") + } + + def filename = args[1] + + String skip = optionValue('skip') ?: '0' + + if (!skip.isNumber()) { + throw new DatabaseMigrationException("The change set skip argument '$count' isn't a number") + } + + configureLiquibase() + + withLiquibase { Liquibase liquibase -> + withDatabase { Database database -> + withFileOrSystemOutWriter(filename) { Writer output -> + doGeneratePreviousChangesetSql(output, database, liquibase, count, skip) + } + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmReleaseLocksCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmReleaseLocksCommand.groovy new file mode 100644 index 00000000000..61ad05209bf --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmReleaseLocksCommand.groovy @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmReleaseLocksCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Releases all locks on the database changelog' + + void handle() { + withLiquibase { Liquibase liquibase -> + liquibase.forceReleaseLocks() + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCommand.groovy new file mode 100644 index 00000000000..416ba3c6a42 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCommand.groovy @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmRollbackCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Rolls back the database to the state it was in when the tag was applied' + + @Override + void handle() { + def tagName = args[0] + if (!tagName) { + throw new DatabaseMigrationException("The $name command requires a tag") + } + + withLiquibase { Liquibase liquibase -> + liquibase.rollback(tagName, contexts) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCountCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCountCommand.groovy new file mode 100644 index 00000000000..dc61205b9b2 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCountCommand.groovy @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmRollbackCountCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Rolls back the specified number of change sets' + + @Override + void handle() { + def number = args[0] + if (!number) { + throw new DatabaseMigrationException("The $name command requires a change set number argument") + } + if (!number.isNumber()) { + throw new DatabaseMigrationException("The change set number argument '$number' isn't a number") + } + + withLiquibase { Liquibase liquibase -> + liquibase.rollback(number.toInteger(), contexts) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCountSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCountSqlCommand.groovy new file mode 100644 index 00000000000..a9b0a2c3137 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackCountSqlCommand.groovy @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmRollbackCountSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Writes the SQL to roll back the specified number of change sets to STDOUT or a file' + + @Override + void handle() { + def number = args[0] + if (!number) { + throw new DatabaseMigrationException("The $name command requires a change set number argument") + } + if (!number.isNumber()) { + throw new DatabaseMigrationException("The change set number argument '$number' isn't a number") + } + + def filename = args[1] + + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.rollback(number.toInteger(), contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackSqlCommand.groovy new file mode 100644 index 00000000000..bea1a2c8bb6 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackSqlCommand.groovy @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmRollbackSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Writes SQL to roll back the database to the state it was in when the tag was applied to STDOUT or a file' + + @Override + void handle() { + def tagName = args[0] + if (!tagName) { + throw new DatabaseMigrationException("The $name command requires a tag") + } + + def filename = args[1] + + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.rollback(tagName, contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackToDateCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackToDateCommand.groovy new file mode 100644 index 00000000000..2b3701a8ef8 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackToDateCommand.groovy @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import java.text.ParseException + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmRollbackToDateCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Rolls back the database to the state it was in at the given date/time' + + @Override + void handle() { + def dateStr = args[0] + if (!dateStr) { + throw new DatabaseMigrationException('Date must be specified as two strings with the format "yyyy-MM-dd HH:mm:ss" or as one strings with the format "yyyy-MM-dd"') + } + + def timeStr = args[1] + + def date = null + try { + date = parseDateTime(dateStr, timeStr) + } catch (ParseException e) { + throw new DatabaseMigrationException("Problem parsing '$dateStr${timeStr ? " $timeStr" : ''}' as a Date: $e.message") + } + + withLiquibase { Liquibase liquibase -> + liquibase.rollback(date, contexts) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackToDateSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackToDateSqlCommand.groovy new file mode 100644 index 00000000000..c263063c92d --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmRollbackToDateSqlCommand.groovy @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import java.text.ParseException + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmRollbackToDateSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Writes SQL to roll back the database to the state it was in at the given date/time to STDOUT or a file' + + @Override + void handle() { + def dateStr = args[0] + if (!dateStr) { + throw new DatabaseMigrationException('Date must be specified as two strings with the format "yyyy-MM-dd HH:mm:ss" or as one strings with the format "yyyy-MM-dd"') + } + + String timeStr = null + String filename = null + if (args[1]) { + if (args.size() > 2 || isTimeFormat(args[1])) { + timeStr = args[1] + } else { + filename = args[1] + } + } + + def date = null + try { + date = parseDateTime(dateStr, timeStr) + } catch (ParseException e) { + throw new DatabaseMigrationException("Problem parsing '$dateStr${timeStr ? " $timeStr" : ''}' as a Date: $e.message") + } + + filename = filename ?: args[2] + + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.rollback(date, contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmStatusCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmStatusCommand.groovy new file mode 100644 index 00000000000..3465df19d63 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmStatusCommand.groovy @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmStatusCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Outputs count or list of unrun change sets to STDOUT or a file' + + void handle() { + def filename = args[0] + def verbose = hasOption('verbose') ? Boolean.parseBoolean(optionValue('verbose')) as Boolean : true + + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.reportStatus(verbose, contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmTagCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmTagCommand.groovy new file mode 100644 index 00000000000..ba5e30d591a --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmTagCommand.groovy @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmTagCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Adds a tag to mark the current database state' + + void handle() { + def tagName = args[0] + if (!tagName) { + throw new DatabaseMigrationException("The $name command requires a tag") + } + + withLiquibase { Liquibase liquibase -> + liquibase.tag(tagName) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCommand.groovy new file mode 100644 index 00000000000..2306b00c1da --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCommand.groovy @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmUpdateCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Updates a database to the current version' + + @Override + void handle() { + withLiquibase { Liquibase liquibase -> + withTransaction { + liquibase.update(contexts) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCountCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCountCommand.groovy new file mode 100644 index 00000000000..663c73dfc1c --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCountCommand.groovy @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmUpdateCountCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Applies next NUM changes to the database' + + @Override + void handle() { + def number = args[0] + if (!number) { + throw new DatabaseMigrationException("The $name command requires a change set number argument") + } + if (!number.isNumber()) { + throw new DatabaseMigrationException("The change set number argument '$number' isn't a number") + } + + withLiquibase { Liquibase liquibase -> + liquibase.update(number.toInteger(), contexts) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCountSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCountSqlCommand.groovy new file mode 100644 index 00000000000..be681a37b88 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateCountSqlCommand.groovy @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmUpdateCountSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Writes the SQL that will partially update a database to STDOUT or a file' + + @Override + void handle() { + def number = args[0] + if (!number) { + throw new DatabaseMigrationException("The $name command requires a change set number argument") + } + if (!number.isNumber()) { + throw new DatabaseMigrationException("The change set number argument '$number' isn't a number") + } + + def filename = args[1] + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.update(number.toInteger(), contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateSqlCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateSqlCommand.groovy new file mode 100644 index 00000000000..a58602ef5bb --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmUpdateSqlCommand.groovy @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmUpdateSqlCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Writes the SQL that will update the database to the current version to STDOUT or a file' + + @Override + void handle() { + def filename = args[0] + + withLiquibase { Liquibase liquibase -> + withFileOrSystemOutWriter(filename) { Writer writer -> + liquibase.update(contexts, writer) + } + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmValidateCommand.groovy b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmValidateCommand.groovy new file mode 100644 index 00000000000..d62c81b5588 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/commands/org/grails/plugins/databasemigration/command/DbmValidateCommand.groovy @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.Liquibase + +import grails.dev.commands.ApplicationCommand + +@CompileStatic +class DbmValidateCommand implements ApplicationCommand, ApplicationContextDatabaseMigrationCommand { + + final String description = 'Checks the changelog for errors' + + void handle() { + withLiquibase { Liquibase liquibase -> + liquibase.validate() + } + } +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/conf/application.yml b/grails-data-hibernate7/dbmigration/grails-app/conf/application.yml new file mode 100644 index 00000000000..585ba3304f0 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/conf/application.yml @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +grails: + profile: web-plugin + codegen: + defaultPackage: databasemigration +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +spring: + groovy: + template: + check-template-location: false diff --git a/grails-data-hibernate7/dbmigration/grails-app/conf/logback.groovy b/grails-data-hibernate7/dbmigration/grails-app/conf/logback.groovy new file mode 100644 index 00000000000..fb6b43c55aa --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/conf/logback.groovy @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import grails.util.BuildSettings +import grails.util.Environment +import org.springframework.boot.logging.logback.ColorConverter +import org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter + +import java.nio.charset.StandardCharsets + +conversionRule('clr', ColorConverter) +conversionRule('wex', WhitespaceThrowableProxyConverter) + +// See http://logback.qos.ch/manual/groovy.html for details on configuration +appender('STDOUT', ConsoleAppender) { + encoder(PatternLayoutEncoder) { + charset = StandardCharsets.UTF_8 + + pattern = + '%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} ' + // Date + '%clr(%5p) ' + // Log level + '%clr(---){faint} %clr([%15.15t]){faint} ' + // Thread + '%clr(%-40.40logger{39}){cyan} %clr(:){faint} ' + // Logger + '%m%n%wex' // Message + } +} + +def targetDir = BuildSettings.TARGET_DIR +if (Environment.isDevelopmentMode() && targetDir != null) { + appender("FULL_STACKTRACE", FileAppender) { + file = "${targetDir}/stacktrace.log" + append = true + encoder(PatternLayoutEncoder) { + charset = StandardCharsets.UTF_8 + pattern = "%level %logger - %msg%n" + } + } + logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) +} +root(ERROR, ['STDOUT']) diff --git a/grails-data-hibernate7/dbmigration/grails-app/domain/testapp/Account.groovy b/grails-data-hibernate7/dbmigration/grails-app/domain/testapp/Account.groovy new file mode 100644 index 00000000000..8bb9041bbc2 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/domain/testapp/Account.groovy @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package testapp + +class Account { + + String name + String number +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/domain/testapp/Person.groovy b/grails-data-hibernate7/dbmigration/grails-app/domain/testapp/Person.groovy new file mode 100644 index 00000000000..eacc974084d --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/domain/testapp/Person.groovy @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package testapp + +class Person { + + String firstName + String lastName + String gender + Integer age + + String emailAddress + String cell +} diff --git a/grails-data-hibernate7/dbmigration/grails-app/init/databasemigration/Application.groovy b/grails-data-hibernate7/dbmigration/grails-app/init/databasemigration/Application.groovy new file mode 100644 index 00000000000..7e4201eb57f --- /dev/null +++ b/grails-data-hibernate7/dbmigration/grails-app/init/databasemigration/Application.groovy @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package databasemigration + +import groovy.transform.CompileStatic + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration +import grails.plugins.metadata.PluginSource + +@PluginSource +@CompileStatic +class Application extends GrailsAutoConfiguration { + + static void main(String[] args) { + GrailsApp.run(Application) + } +} diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/AutoRunWithMultipleDataSourceSpec.groovy b/grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/AutoRunWithMultipleDataSourceSpec.groovy new file mode 100644 index 00000000000..1abaaf2197a --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/AutoRunWithMultipleDataSourceSpec.groovy @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration + +import grails.testing.mixin.integration.Integration +import groovy.sql.Sql +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ActiveProfiles +import spock.lang.AutoCleanup +import spock.lang.Specification + +import javax.sql.DataSource + +@Integration +@ActiveProfiles('multiple-datasource') +class AutoRunWithMultipleDataSourceSpec extends Specification { + + @Autowired + DataSource dataSource + + @Autowired + DataSource dataSource_second + + @AutoCleanup + Sql sql + + @AutoCleanup + Sql secondSql + + def setup() { + sql = new Sql(dataSource) + secondSql = new Sql(dataSource_second) + } + + def "runs app with a multiple datasource"() { + when: + def changeSetIds = sql.rows('SELECT id FROM DATABASECHANGELOG').collect { it.id } + + then: + changeSetIds as Set == ['1', '2', '3', '4', '5'] as Set + + when: + def secondChangeSetIds = secondSql.rows('SELECT id FROM DATABASECHANGELOG').collect { it.id } + + then: + secondChangeSetIds as Set == ['second-1', 'second-2', 'second-3'] as Set + } +} diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/AutoRunWithSingleDataSourceSpec.groovy b/grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/AutoRunWithSingleDataSourceSpec.groovy new file mode 100644 index 00000000000..725e2f22e79 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/AutoRunWithSingleDataSourceSpec.groovy @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration + +import grails.testing.mixin.integration.Integration +import groovy.sql.Sql +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ActiveProfiles +import spock.lang.AutoCleanup +import spock.lang.Specification + +import javax.sql.DataSource + +@Integration +@ActiveProfiles('single-datasource') +class AutoRunWithSingleDataSourceSpec extends Specification { + + @Autowired + DataSource dataSource + + @AutoCleanup + Sql sql + + def setup() { + sql = new Sql(dataSource) + //sql.executeUpdate("drop table AUTHOR") + } + + def "runs app with a single datasource"() { + expect: + def changeSetIds = sql.rows('SELECT id FROM databasechangelog').collect { it.id } + changeSetIds as Set == ['1', '2', '3', '5'] as Set + + and: + def authors = sql.rows('SELECT name FROM author').collect { it.name } + authors == ['Amelia'] + } +} diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/DbUpdateCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/DbUpdateCommandSpec.groovy new file mode 100644 index 00000000000..466e124794a --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/groovy/org/grails/plugins/databasemigration/DbUpdateCommandSpec.groovy @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.plugins.databasemigration + +import grails.dev.commands.ApplicationCommand +import grails.dev.commands.ExecutionContext +import grails.testing.mixin.integration.Integration +import grails.util.GrailsNameUtils +import groovy.sql.Sql +import liquibase.GlobalConfiguration +import liquibase.Scope +import liquibase.exception.LiquibaseException +import org.grails.build.parsing.CommandLineParser +import org.grails.plugins.databasemigration.command.DbmUpdateCommand +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.stereotype.Component +import org.springframework.test.context.ActiveProfiles +import spock.lang.AutoCleanup +import spock.lang.Specification + +import javax.sql.DataSource + +@Integration +@ActiveProfiles('transaction-datasource') +@Component +class DbUpdateCommandSpec extends Specification { + + @Autowired + DataSource dataSource + + @Autowired + ApplicationContext applicationContext + + @AutoCleanup + Sql sql + + def setup() { + sql = new Sql(dataSource) + } + + void "test the transaction behaviour in the changeSet with grailsChange and GORM"() { + + when: + Scope.child(GlobalConfiguration.DUPLICATE_FILE_MODE.getKey(), GlobalConfiguration.DuplicateFileMode.WARN, { -> + DbmUpdateCommand command = new DbmUpdateCommand() + command.applicationContext = applicationContext + command.setExecutionContext(getExecutionContext(DbmUpdateCommand)) + command.handle() + } as Scope.ScopedRunner) + + then: + def e = thrown(LiquibaseException) + e.cause instanceof LiquibaseException + sql.firstRow('SELECT COUNT(*) AS num FROM DATABASECHANGELOG WHERE id=?;', 'create-person-grails').num == 1 + sql.firstRow('SELECT COUNT(*) AS num FROM person;').num == 1 + sql.firstRow('SELECT COUNT(*) AS num FROM account;').num == 0 + + } + + private ExecutionContext getExecutionContext(Class clazz, String... args) { + def commandClassName = GrailsNameUtils.getScriptName(GrailsNameUtils.getLogicalName(clazz.name, 'Command')) + new ExecutionContext( + new CommandLineParser().parse(([commandClassName] + args.toList()) as String[]) + ) + } +} + + + + diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/application-multiple-datasource.yml b/grails-data-hibernate7/dbmigration/src/integration-test/resources/application-multiple-datasource.yml new file mode 100644 index 00000000000..1c8e330fe22 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/application-multiple-datasource.yml @@ -0,0 +1,44 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +grails: + plugin: + databasemigration: + updateOnStart: true + second: + updateOnStart: true +--- +server: + port: 0 +--- +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + dbCreate: none + url: jdbc:h2:file:./multipleFirstDb + logSql: true + formatSql: true +dataSources: + second: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + dbCreate: none + url: jdbc:h2:file:./multipleSecondDb \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/application-single-datasource.yml b/grails-data-hibernate7/dbmigration/src/integration-test/resources/application-single-datasource.yml new file mode 100644 index 00000000000..8cfd9780170 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/application-single-datasource.yml @@ -0,0 +1,33 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +grails: + plugin: + databasemigration: + updateOnStart: true + updateOnStartContexts: + - test +--- +server: + port: 0 +--- +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + dbCreate: none + url: jdbc:h2:file:./singleDb diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/application-transaction-datasource.yml b/grails-data-hibernate7/dbmigration/src/integration-test/resources/application-transaction-datasource.yml new file mode 100644 index 00000000000..297a5fdcfd1 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/application-transaction-datasource.yml @@ -0,0 +1,44 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +grails: + plugin: + databasemigration: + changelogFileName: 'changelog-transaction.groovy' + changelogLocation: 'src/integration-test/resources' + updateOnStart: false + second: + updateOnStart: false +--- +server: + port: 0 +--- +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + dbCreate: none + url: jdbc:h2:file:./testDb +dataSources: + other: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + dbCreate: none + url: jdbc:h2:file:./otherDb diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-account-person-init.groovy b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-account-person-init.groovy new file mode 100644 index 00000000000..e4c82cdb56d --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-account-person-init.groovy @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +databaseChangeLog = { + changeSet(id: "create-person-table", author: 'integration-test') { + createTable(tableName: "person") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "first_name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + + column(name: "age", type: "INT") { + constraints(nullable: "false") + } + + column(name: "gender", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + + column(name: "last_name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + + column(name: "cell", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + + column(name: "email_address", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(id: "create-account-table", author: 'integration-test') { + createTable(tableName: "account") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "accountPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + + column(name: "number", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-account-sql.groovy b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-account-sql.groovy new file mode 100644 index 00000000000..5bcba045e32 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-account-sql.groovy @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +databaseChangeLog = { + changeSet(id: 'create-account-sql', author: 'integration-test') { + sql "INSERT INTO account (version, name) VALUES (0, 'Joseph');" + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-person-grails.groovy b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-person-grails.groovy new file mode 100644 index 00000000000..4a7f3c630da --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-person-grails.groovy @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import testapp.Person + +databaseChangeLog = { + changeSet(id: 'create-person-grails', author: 'integration-test') { + + grailsChange { + change { + Person person = new Person() + person.firstName = 'Joseph1' + person.lastName = 'Holmes' + person.age = 56 + person.gender = 'male' + person.cell = '734-776-7738' + person.emailAddress = 'jhomes@example.com' + person.save(flush: true, failOnError: true) + } + rollback { + confirm('Done: Rollback person Jone') + } + } + } +} \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-second.groovy b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-second.groovy new file mode 100644 index 00000000000..4500e5df104 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-second.groovy @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +databaseChangeLog = { + + changeSet(author: "John Smith", id: "second-1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "second-2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "second-3") { + addForeignKeyConstraint(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK_4sac2ubmnqva85r8bk8fxdvbf", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author") + } +} diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-transaction.groovy b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-transaction.groovy new file mode 100644 index 00000000000..0a92b420673 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog-transaction.groovy @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +databaseChangeLog = { + include file: 'changelog-account-person-init.groovy' + include file: 'changelog-person-grails.groovy' + include file: 'changelog-account-sql.groovy' +} \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog.groovy b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog.groovy new file mode 100644 index 00000000000..e5433a4841d --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/changelog.groovy @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "3") { + addForeignKeyConstraint(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK_4sac2ubmnqva85r8bk8fxdvbf", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author") + } + + changeSet(author: "John Smith", id: "4", context: "development") { + insert(tableName: "author") { + column(name: "name", value: "Mary") + column(name: "version", value: "0") + } + } + + changeSet(author: "John Smith", id: "5", context: "test") { + insert(tableName: "author") { + column(name: "name", value: "Amelia") + column(name: "version", value: "0") + } + } +} diff --git a/grails-data-hibernate7/dbmigration/src/integration-test/resources/logback-test.xml b/grails-data-hibernate7/dbmigration/src/integration-test/resources/logback-test.xml new file mode 100644 index 00000000000..c78478c2bd3 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/integration-test/resources/logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + + + \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationException.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationException.groovy new file mode 100644 index 00000000000..f1a23996763 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationException.groovy @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration + +import groovy.transform.InheritConstructors + +@InheritConstructors +class DatabaseMigrationException extends RuntimeException {} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationGrailsPlugin.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationGrailsPlugin.groovy new file mode 100644 index 00000000000..51d297c8310 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationGrailsPlugin.groovy @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration + +import javax.sql.DataSource + +import liquibase.parser.ChangeLogParser +import liquibase.parser.ChangeLogParserFactory + +import org.springframework.context.ApplicationContext + +import grails.plugins.Plugin +import org.grails.plugins.databasemigration.liquibase.GrailsLiquibase +import org.grails.plugins.databasemigration.liquibase.GrailsLiquibaseFactory +import org.grails.plugins.databasemigration.liquibase.GroovyChangeLogParser + +class DatabaseMigrationGrailsPlugin extends Plugin { + + static final String CONFIG_MAIN_PREFIX = 'grails.plugin.databasemigration' + + def grailsVersion = '7.0.0-SNAPSHOT > *' + def pluginExcludes = [ + '**/testapp/**', + 'grails-app/views/error.gsp' + ] + + def title = 'Grails Database Migration Plugin' // Headline display name of the plugin + def author = 'Kazuki YAMAMOTO' + def authorEmail = '' + def description = 'Grails Database Migration Plugin' + def documentation = 'https://grails.apache.org/docs/latest/grails-data/hibernate5/manual/index.html#databaseMigration' + def license = 'APACHE' + def scm = [url: 'https://github.com/apache/grails-core'] + + @Override + Closure doWithSpring() { + configureLiquibase() + return { -> + grailsLiquibaseFactory(GrailsLiquibaseFactory, applicationContext) + } + } + + @Override + void doWithApplicationContext() { + def mainClassName = deduceApplicationMainClassName() + + def updateAllOnStart = config.getProperty("${CONFIG_MAIN_PREFIX}.updateAllOnStart", Boolean, false) + + dataSourceNames.each { String dataSourceName -> + String configPrefix = isDefaultDataSource(dataSourceName) ? CONFIG_MAIN_PREFIX : "${CONFIG_MAIN_PREFIX}.${dataSourceName}" + def skipMainClasses = config.getProperty("${configPrefix}.skipUpdateOnStartMainClasses", List, ['grails.ui.command.GrailsApplicationContextCommandRunner']) + if (skipMainClasses.contains(mainClassName)) { + return + } + + if (!updateAllOnStart) { + def updateOnStart = config.getProperty("${configPrefix}.updateOnStart", Boolean, false) + if (!updateOnStart) { + return + } + } else { + configPrefix = CONFIG_MAIN_PREFIX + } + + new DatabaseMigrationTransactionManager(applicationContext, dataSourceName).withTransaction { + GrailsLiquibase gl = applicationContext.getBean('grailsLiquibaseFactory', GrailsLiquibase) + gl.dataSource = getDataSourceBean(applicationContext, dataSourceName) + gl.dropFirst = config.getProperty("${configPrefix}.dropOnStart", Boolean, false) + gl.changeLog = config.getProperty("${configPrefix}.updateOnStartFileName", String, isDefaultDataSource(dataSourceName) ? 'changelog.groovy' : "changelog-${dataSourceName}.groovy") + gl.contexts = config.getProperty("${configPrefix}.updateOnStartContexts", List, []).join(',') + gl.labels = config.getProperty("${configPrefix}.updateOnStartLabels", List, []).join(',') + gl.defaultSchema = config.getProperty("${configPrefix}.updateOnStartDefaultSchema", String) + gl.databaseChangeLogTableName = config.getProperty("${configPrefix}.databaseChangeLogTableName", String) + gl.databaseChangeLogLockTableName = config.getProperty("${configPrefix}.databaseChangeLogLockTableName", String) + gl.dataSourceName = getDataSourceName(dataSourceName) + gl.afterPropertiesSet() + } + } + } + + private def getDataSourceBean(ApplicationContext applicationContext, String dataSourceName) { + applicationContext.getBean(getDataSourceName(dataSourceName), DataSource) + } + + private void configureLiquibase() { + def groovyChangeLogParser = ChangeLogParserFactory.instance.parsers.find { ChangeLogParser changeLogParser -> changeLogParser instanceof GroovyChangeLogParser } as GroovyChangeLogParser + groovyChangeLogParser.applicationContext = applicationContext + groovyChangeLogParser.config = config + } + + private Set getDataSourceNames() { + def dataSources = config.getProperty('dataSources', Map, [:]) + if (!dataSources) { + return ['dataSource'] + } + Set dataSourceNames = dataSources.keySet() + if (!dataSourceNames.contains('dataSource')) { + dataSourceNames = ['dataSource'] + dataSourceNames + } + dataSourceNames + } + + private String deduceApplicationMainClassName() { + new RuntimeException().stackTrace.find { StackTraceElement stackTraceElement -> 'main' == stackTraceElement.methodName }?.className + } + + static String getDataSourceName(String dataSourceName) { + isDefaultDataSource(dataSourceName) ? dataSourceName : "dataSource_$dataSourceName" + } + + static Boolean isDefaultDataSource(String dataSourceName) { + !dataSourceName || 'dataSource' == dataSourceName + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationTransactionManager.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationTransactionManager.groovy new file mode 100644 index 00000000000..eabb3b549e4 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/DatabaseMigrationTransactionManager.groovy @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.plugins.databasemigration + +import org.springframework.context.ApplicationContext +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.TransactionDefinition +import org.springframework.transaction.support.DefaultTransactionDefinition +import org.springframework.util.Assert + +import grails.gorm.transactions.GrailsTransactionTemplate + +/** + * Created by Jim on 7/15/2016. + */ +class DatabaseMigrationTransactionManager { + + final String dataSource + final ApplicationContext applicationContext + + DatabaseMigrationTransactionManager(ApplicationContext applicationContext, String dataSource) { + this.dataSource = dataSource + this.applicationContext = applicationContext + } + + /** + * + * @return The transactionManager bean for the current dataSource + */ + PlatformTransactionManager getTransactionManager() { + String dataSource = this.dataSource ?: 'dataSource' + String beanName = 'transactionManager' + if (dataSource != 'dataSource') { + beanName += "_${dataSource}" + } + applicationContext.getBean(beanName, PlatformTransactionManager) + } + + /** + * Executes the closure within the context of a transaction, creating one if none is present or joining + * an existing transaction if one is already present. + * + * @param callable The closure to call + * @return The result of the closure execution + * @see #withTransaction(Map, Closure) + * @see #withNewTransaction(Closure) + * @see #withNewTransaction(Map, Closure) + */ + void withTransaction(Closure callable) { + withTransaction(new DefaultTransactionDefinition(), callable) + } + + /** + * Executes the closure within the context of a new transaction + * + * @param callable The closure to call + * @return The result of the closure execution + * @see #withTransaction(Closure) + * @see #withTransaction(Map, Closure) + * @see #withNewTransaction(Map, Closure) + */ + void withNewTransaction(Closure callable) { + withTransaction([propagationBehavior: TransactionDefinition.PROPAGATION_REQUIRES_NEW], callable) + } + + /** + * Executes the closure within the context of a new transaction which is + * configured with the properties contained in transactionProperties. + * transactionProperties may contain any properties supported by + * {@link DefaultTransactionDefinition}. Note that if transactionProperties + * includes entries for propagationBehavior or propagationName, those values + * will be ignored. This method always sets the propagation level to + * TransactionDefinition.REQUIRES_NEW. + * + *

+ *
+     * SomeEntity.withNewTransaction([isolationLevel: TransactionDefinition.ISOLATION_REPEATABLE_READ]) {
+     *     // ...
+     * }
+     * 
+ *
+ * + * @param transactionProperties properties to configure the transaction properties + * @param callable The closure to call + * @return The result of the closure execution + * @see DefaultTransactionDefinition + * @see #withNewTransaction(Closure) + * @see #withTransaction(Closure) + * @see #withTransaction(Map, Closure) + */ + void withNewTransaction(Map transactionProperties, Closure callable) { + def props = new HashMap(transactionProperties) + props.remove('propagationName') + props.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW + withTransaction(props, callable) + } + + void withTransaction(Map transactionProperties, Closure callable) { + def transactionDefinition = new DefaultTransactionDefinition() + transactionProperties.each { k, v -> + if (v instanceof CharSequence && !(v instanceof String)) { + v = v.toString() + } + try { + transactionDefinition[k as String] = v + } catch (MissingPropertyException mpe) { + throw new IllegalArgumentException("[${k}] is not a valid transaction property.", mpe) + } + } + withTransaction(transactionDefinition, callable) + } + + /** + * Executes the closure within the context of a transaction for the given {@link TransactionDefinition} + * + * @param callable The closure to call + * @return The result of the closure execution + */ + void withTransaction(TransactionDefinition definition, Closure callable) { + Assert.notNull(transactionManager, 'No transactionManager bean configured') + + if (!callable) { + return + } + + new GrailsTransactionTemplate(transactionManager, definition).execute(callable) + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/EnvironmentAwareCodeGenConfig.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/EnvironmentAwareCodeGenConfig.groovy new file mode 100644 index 00000000000..826aa19e06a --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/EnvironmentAwareCodeGenConfig.groovy @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import org.grails.config.CodeGenConfig + +@CompileStatic +class EnvironmentAwareCodeGenConfig extends CodeGenConfig { + + EnvironmentAwareCodeGenConfig(CodeGenConfig copyOf, String environment) { + super(copyOf) + mergeEnvironmentConfig(copyOf, environment) + } + + @CompileDynamic + private void mergeEnvironmentConfig(CodeGenConfig copyOf, String environment) { + mergeMap(copyOf.environments?."$environment" ?: [:]) + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/NoopVisitor.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/NoopVisitor.groovy new file mode 100644 index 00000000000..09baef6d3fa --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/NoopVisitor.groovy @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.plugins.databasemigration + +import liquibase.changelog.ChangeSet +import liquibase.changelog.DatabaseChangeLog +import liquibase.changelog.filter.ChangeSetFilterResult +import liquibase.changelog.visitor.ChangeSetVisitor +import liquibase.database.Database +import liquibase.exception.LiquibaseException + +class NoopVisitor implements ChangeSetVisitor { + + protected Database database + + NoopVisitor(Database database) { + this.database = database + } + + Direction getDirection() { Direction.FORWARD } + + @Override + void visit(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database, Set filterResults) throws LiquibaseException { + changeSet.execute(databaseChangeLog, database) + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/PluginConstants.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/PluginConstants.groovy new file mode 100644 index 00000000000..20f039fce06 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/PluginConstants.groovy @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.plugins.databasemigration + +class PluginConstants { + + static final String DATA_SOURCE_NAME_KEY = 'dataSourceName' + static final String DEFAULT_DATASOURCE_NAME = 'dataSource' + static final String DEFAULT_CHANGE_LOG_LOCATION = 'grails-app/migrations' +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommand.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommand.groovy new file mode 100644 index 00000000000..26c4a9cf7c9 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommand.groovy @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType + +import liquibase.database.Database +import liquibase.parser.ChangeLogParser +import liquibase.parser.ChangeLogParserFactory +import org.hibernate.dialect.Dialect +import org.hibernate.engine.jdbc.spi.JdbcServices +import org.hibernate.engine.spi.SessionFactoryImplementor + +import org.springframework.context.ConfigurableApplicationContext + +import grails.config.ConfigMap +import grails.core.GrailsApplication +import grails.dev.commands.ExecutionContext +import grails.util.Environment +import org.grails.config.PropertySourcesConfig +import org.grails.orm.hibernate.HibernateDatastore +import org.grails.plugins.databasemigration.DatabaseMigrationTransactionManager +import org.grails.plugins.databasemigration.liquibase.GormDatabase +import org.grails.plugins.databasemigration.liquibase.GroovyChangeLogParser + +import static org.grails.plugins.databasemigration.DatabaseMigrationGrailsPlugin.isDefaultDataSource +import static org.grails.plugins.databasemigration.PluginConstants.DEFAULT_DATASOURCE_NAME + +@CompileStatic +trait ApplicationContextDatabaseMigrationCommand implements DatabaseMigrationCommand { + + ConfigurableApplicationContext applicationContext + + Boolean skipBootstrap = true + + boolean handle(ExecutionContext executionContext) { + this.executionContext = executionContext + handle() + return true + } + + void setExecutionContext(ExecutionContext executionContext) { + this.commandLine = executionContext.commandLine + this.contexts = optionValue('contexts') + this.defaultSchema = optionValue('defaultSchema') + this.dataSource = optionValue('dataSource') ?: DEFAULT_DATASOURCE_NAME + } + + abstract void handle() + + @Override + ConfigMap getConfig() { + applicationContext.getBean(GrailsApplication).config + } + + void withGormDatabase(ConfigurableApplicationContext applicationContext, String dataSource, + @ClosureParams(value = SimpleType, options = 'liquibase.database.Database') Closure closure) { + def database = null + try { + database = createGormDatabase(applicationContext, dataSource) + closure.call(database) + } finally { + database?.close() + } + } + + private Database createGormDatabase(ConfigurableApplicationContext applicationContext, String dataSource) { + String sessionFactoryName = 'sessionFactory' + if (!isDefaultDataSource(dataSource)) { + sessionFactoryName = sessionFactoryName + '_' + dataSource + } + + def serviceRegistry = applicationContext.getBean(sessionFactoryName, SessionFactoryImplementor).serviceRegistry.parentServiceRegistry + + Dialect dialect = serviceRegistry.getService(JdbcServices).dialect + + HibernateDatastore hibernateDatastore = applicationContext.getBean('hibernateDatastore', HibernateDatastore) + hibernateDatastore = hibernateDatastore.getDatastoreForConnection(dataSource) + + Database database = new GormDatabase(dialect, serviceRegistry, hibernateDatastore) + configureDatabase(database) + + return database + } + + ConfigMap getEnvironmentConfig(String environment) { + return (ConfigMap) environmentWith(environment) { + new PropertySourcesConfig(((PropertySourcesConfig) config).getPropertySources()) + } + } + + private Object environmentWith(String environment, Closure closure) { + def originalEnvironment = Environment.current + System.setProperty(Environment.KEY, environment) + try { + return closure.call() + } finally { + System.setProperty(Environment.KEY, originalEnvironment.name) + } + } + + void withTransaction(Closure callable) { + new DatabaseMigrationTransactionManager(this.applicationContext, this.dataSource).withTransaction(callable) + } + + void configureLiquibase() { + def groovyChangeLogParser = ChangeLogParserFactory.instance.parsers.find { ChangeLogParser changeLogParser -> changeLogParser instanceof GroovyChangeLogParser } as GroovyChangeLogParser + groovyChangeLogParser.applicationContext = applicationContext + groovyChangeLogParser.config = config + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommand.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommand.groovy new file mode 100644 index 00000000000..a397467fda4 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommand.groovy @@ -0,0 +1,429 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import java.nio.file.Path +import java.text.DateFormat +import java.text.ParseException +import java.text.SimpleDateFormat + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType + +import liquibase.Contexts +import liquibase.LabelExpression +import liquibase.Liquibase +import liquibase.RuntimeEnvironment +import liquibase.Scope +import liquibase.changelog.ChangeLogIterator +import liquibase.changelog.DatabaseChangeLog +import liquibase.changelog.filter.ContextChangeSetFilter +import liquibase.changelog.filter.CountChangeSetFilter +import liquibase.changelog.filter.DbmsChangeSetFilter +import liquibase.command.CommandScope +import liquibase.command.core.DiffChangelogCommandStep +import liquibase.command.core.DiffCommandStep +import liquibase.command.core.GenerateChangelogCommandStep +import liquibase.command.core.helpers.AbstractChangelogCommandStep +import liquibase.command.core.helpers.DbUrlConnectionArgumentsCommandStep +import liquibase.command.core.helpers.DiffOutputControlCommandStep +import liquibase.command.core.helpers.PreCompareCommandStep +import liquibase.command.core.helpers.ReferenceDbUrlConnectionCommandStep +import liquibase.database.Database +import liquibase.database.DatabaseConnection +import liquibase.database.DatabaseFactory +import liquibase.database.core.MSSQLDatabase +import liquibase.database.core.OracleDatabase +import liquibase.diff.compare.CompareControl +import liquibase.diff.output.DiffOutputControl +import liquibase.diff.output.StandardObjectChangeFilter +import liquibase.exception.DatabaseException +import liquibase.exception.LiquibaseException +import liquibase.exception.LockException +import liquibase.executor.Executor +import liquibase.executor.ExecutorService +import liquibase.executor.LoggingExecutor +import liquibase.lockservice.LockService +import liquibase.lockservice.LockServiceFactory +import liquibase.parser.ChangeLogParserFactory +import liquibase.resource.ClassLoaderResourceAccessor +import liquibase.resource.CompositeResourceAccessor +import liquibase.resource.FileSystemResourceAccessor +import liquibase.resource.ResourceAccessor +import liquibase.statement.core.RawSqlStatement +import liquibase.structure.core.Catalog +import liquibase.util.LiquibaseUtil +import liquibase.util.StreamUtil + +import grails.config.ConfigMap +import org.grails.build.parsing.CommandLine +import org.grails.plugins.databasemigration.DatabaseMigrationException +import org.grails.plugins.databasemigration.NoopVisitor + +import static org.grails.plugins.databasemigration.DatabaseMigrationGrailsPlugin.getDataSourceName +import static org.grails.plugins.databasemigration.DatabaseMigrationGrailsPlugin.isDefaultDataSource +import static org.grails.plugins.databasemigration.PluginConstants.DATA_SOURCE_NAME_KEY +import static org.grails.plugins.databasemigration.PluginConstants.DEFAULT_CHANGE_LOG_LOCATION + +@CompileStatic +trait DatabaseMigrationCommand { + + CommandLine commandLine + + String defaultSchema + String dataSource + String contexts + + abstract ConfigMap getConfig() + + String optionValue(String name) { + commandLine.optionValue(name)?.toString() + } + + boolean hasOption(String name) { + commandLine.hasOption(name) + } + + String getContexts() { + if (contexts) { + return contexts + } + return config.getProperty("${configPrefix}.contexts".toString(), List)?.join(',') + } + + List getArgs() { + commandLine.remainingArgs + } + + File getChangeLogLocation() { + new File(config.getProperty("${configPrefix}.changelogLocation".toString(), String) ?: DEFAULT_CHANGE_LOG_LOCATION) + } + + File getChangeLogFile() { + new File(changeLogLocation, changeLogFileName) + } + + String getChangeLogFileName() { + def changelogFileName = config.getProperty("${configPrefix}.changelogFileName".toString(), String) + if (changelogFileName) { + return changelogFileName + } + return isDefaultDataSource(dataSource) ? 'changelog.groovy' : "changelog-${dataSource}.groovy" + } + + File resolveChangeLogFile(String filename) { + if (!filename) { + return null + } + if (getExtension(filename)) { + return new File(changeLogLocation, filename) + } + if (dataSource) { + return new File(changeLogLocation, "${filename}-${dataSource}.groovy") + } + return new File(changeLogLocation, "${filename}.groovy") + } + + Map getDataSourceConfig(ConfigMap config = this.config) { + def dataSourceName = dataSource ?: 'dataSource' + + if (dataSourceName == 'dataSource' && config.containsKey(dataSourceName)) { + return (Map) (config.getProperty(dataSourceName, Map) ?: [:]) + } + + def dataSources = config.getProperty('dataSources', Map) ?: [:] + if (!dataSources) { + def defaultDataSource = config.getProperty('dataSource', Map) + if (defaultDataSource) { + dataSources['dataSource'] = defaultDataSource + } + } + return (Map) dataSources.get(dataSourceName) + } + + void withFileOrSystemOutWriter(String filename, @ClosureParams(value = SimpleType, options = 'java.io.Writer') Closure closure) { + if (!filename) { + closure.call(new PrintWriter(System.out)) + return + } + + def outputFile = new File(filename) + if (outputFile.parentFile && !outputFile.parentFile.exists()) { + outputFile.parentFile.mkdirs() + } + outputFile.withWriter { BufferedWriter writer -> + closure.call(writer) + } + } + + boolean isTimeFormat(String time) { + time ==~ /\d{2}:\d{2}:\d{2}/ + } + + Date parseDateTime(String date, String time) throws ParseException { + time = time ?: '00:00:00' + DateFormat formatter = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss') + formatter.parse("$date $time") + } + + void withLiquibase(@ClosureParams(value = SimpleType, options = 'liquibase.Liquibase') Closure closure) { + def resourceAccessor = createResourceAccessor() + + Path changeLogLocationPath = changeLogLocation.toPath() + Path changeLogFilePath = changeLogFile.toPath() + String relativePath = changeLogLocationPath.relativize(changeLogFilePath).toString() + + withDatabase { Database database -> + Liquibase liquibase = new Liquibase(relativePath, resourceAccessor, database) + liquibase.changeLogParameters.set(DATA_SOURCE_NAME_KEY, getDataSourceName(dataSource)) + closure.call(liquibase) + } + } + + ResourceAccessor createResourceAccessor() { + // Avoid duplicates when migrations have been copied to the classpath + if (Thread.currentThread().contextClassLoader?.getResource(changeLogFile.name)) { + return new CompositeResourceAccessor(new ClassLoaderResourceAccessor()) + } else { + return new CompositeResourceAccessor(new FileSystemResourceAccessor(changeLogLocation)) + } + } + + void withDatabase(Map dataSourceConfig = null, @ClosureParams(value = SimpleType, options = 'liquibase.database.Database') Closure closure) { + def database = null + try { + database = createDatabase(defaultSchema, dataSource, dataSourceConfig ?: getDataSourceConfig()) + closure.call(database) + } finally { + database?.close() + } + } + + @CompileDynamic + Database createDatabase(String defaultSchema, String dataSource, Map dataSourceConfig) { + String password = dataSourceConfig.password ?: null + + if (password && dataSourceConfig.passwordEncryptionCodec) { + def clazz = Class.forName(dataSourceConfig.passwordEncryptionCodec) + password = clazz.decode(password) + } + + Database database = DatabaseFactory.getInstance().openDatabase( + dataSourceConfig.url, + dataSourceConfig.username ?: null, + password, + dataSourceConfig.driverClassName, + null, + null, + null, + new ClassLoaderResourceAccessor(Thread.currentThread().contextClassLoader) + ) + configureDatabase(database) + return database + } + + void configureDatabase(Database database) { + database.defaultSchemaName = defaultSchema + if (!database.supportsSchemas() && defaultSchema) { + database.defaultCatalogName = defaultSchema + } + database.databaseChangeLogTableName = config.getProperty("${configPrefix}.databaseChangeLogTableName".toString(), String) + database.databaseChangeLogLockTableName = config.getProperty("${configPrefix}.databaseChangeLogLockTableName".toString(), String) + } + + void doGenerateChangeLog(File changeLogFile, Database originalDatabase) { + def changeLogFilePath = changeLogFile?.path + def compareControl = new CompareControl([] as CompareControl.SchemaComparison[], null as String) + DiffOutputControl diffOutputControl = createDiffOutputControl() + + final CommandScope command = new CommandScope('groovyGenerateChangeLog') + command + .addArgumentValue(ReferenceDbUrlConnectionCommandStep.REFERENCE_DATABASE_ARG, originalDatabase) + .addArgumentValue(DbUrlConnectionArgumentsCommandStep.DATABASE_ARG, originalDatabase) + .addArgumentValue(PreCompareCommandStep.SNAPSHOT_TYPES_ARG, DiffCommandStep.parseSnapshotTypes(null as String)) + .addArgumentValue(PreCompareCommandStep.COMPARE_CONTROL_ARG, compareControl) + .addArgumentValue(DiffChangelogCommandStep.CHANGELOG_FILE_ARG, changeLogFilePath) + .addArgumentValue(DiffOutputControlCommandStep.INCLUDE_CATALOG_ARG, diffOutputControl.getIncludeCatalog()) + .addArgumentValue(DiffOutputControlCommandStep.INCLUDE_SCHEMA_ARG, diffOutputControl.getIncludeSchema()) + .addArgumentValue(DiffOutputControlCommandStep.INCLUDE_TABLESPACE_ARG, diffOutputControl.getIncludeTablespace()) + .addArgumentValue(GenerateChangelogCommandStep.OVERWRITE_OUTPUT_FILE_ARG, GenerateChangelogCommandStep.OVERWRITE_OUTPUT_FILE_ARG.getDefaultValue()) + .addArgumentValue(GenerateChangelogCommandStep.RUN_ON_CHANGE_TYPES_ARG, AbstractChangelogCommandStep.RUN_ON_CHANGE_TYPES_ARG.getDefaultValue()) + .addArgumentValue(GenerateChangelogCommandStep.REPLACE_IF_EXISTS_TYPES_ARG, AbstractChangelogCommandStep.REPLACE_IF_EXISTS_TYPES_ARG.getDefaultValue()) + + if (diffOutputControl.isReplaceIfExistsSet()) { + command.addArgumentValue(GenerateChangelogCommandStep.USE_OR_REPLACE_OPTION, true) + } + command.setOutput(System.out) + command.execute() + } + + void doDiffToChangeLog(File changeLogFile, Database referenceDatabase, Database targetDatabase) { + def changeLogFilePath = changeLogFile?.path + def compareControl = new CompareControl([] as CompareControl.SchemaComparison[], null as String) + DiffOutputControl diffOutputControl = createDiffOutputControl() + + final CommandScope command = new CommandScope('groovyDiffChangelog') + command + .addArgumentValue(ReferenceDbUrlConnectionCommandStep.REFERENCE_DATABASE_ARG, referenceDatabase) + .addArgumentValue(DbUrlConnectionArgumentsCommandStep.DATABASE_ARG, targetDatabase) + .addArgumentValue(PreCompareCommandStep.SNAPSHOT_TYPES_ARG, DiffCommandStep.parseSnapshotTypes(null as String)) + .addArgumentValue(PreCompareCommandStep.COMPARE_CONTROL_ARG, compareControl) + .addArgumentValue(PreCompareCommandStep.OBJECT_CHANGE_FILTER_ARG, diffOutputControl.objectChangeFilter) + .addArgumentValue(DiffChangelogCommandStep.CHANGELOG_FILE_ARG, changeLogFilePath) + .addArgumentValue(DiffOutputControlCommandStep.INCLUDE_CATALOG_ARG, diffOutputControl.getIncludeCatalog()) + .addArgumentValue(DiffOutputControlCommandStep.INCLUDE_SCHEMA_ARG, diffOutputControl.getIncludeSchema()) + .addArgumentValue(DiffOutputControlCommandStep.INCLUDE_TABLESPACE_ARG, diffOutputControl.getIncludeTablespace()) + .addArgumentValue(GenerateChangelogCommandStep.RUN_ON_CHANGE_TYPES_ARG, AbstractChangelogCommandStep.RUN_ON_CHANGE_TYPES_ARG.getDefaultValue()) + .addArgumentValue(GenerateChangelogCommandStep.REPLACE_IF_EXISTS_TYPES_ARG, AbstractChangelogCommandStep.REPLACE_IF_EXISTS_TYPES_ARG.getDefaultValue()) + + if (diffOutputControl.isReplaceIfExistsSet()) { + command.addArgumentValue(GenerateChangelogCommandStep.USE_OR_REPLACE_OPTION, true) + } + command.setOutput(System.out) + command.execute() + + } + + void doGeneratePreviousChangesetSql(Writer output, Database database, Liquibase liquibase, String count, String skip) { + Contexts contexts = new Contexts(contexts) + LabelExpression labelExpression = liquibase.changeLogParameters.labels + liquibase.changeLogParameters.setContexts(contexts) + + final ExecutorService executorService = Scope.getCurrentScope().getSingleton(ExecutorService) + final Executor oldTemplate = executorService.getExecutor('jdbc', database) + final LoggingExecutor outputTemplate = new LoggingExecutor(oldTemplate, output, database) + executorService.setExecutor('jdbc', database, outputTemplate) + + outputHeader(outputTemplate, (String) "Previous $count SQL Changeset(s) Skipping $skip Script", liquibase, database) + + LockService lockService = LockServiceFactory.getInstance().getLockService(database) + lockService.waitForLock() + + try { + def parser = ChangeLogParserFactory.instance.getParser(liquibase.changeLogFile, liquibase.resourceAccessor) + DatabaseChangeLog changeLog = parser.parse(liquibase.changeLogFile, liquibase.changeLogParameters, liquibase.resourceAccessor) + liquibase.checkLiquibaseTables(true, changeLog, contexts, labelExpression) + changeLog.validate(database, contexts, labelExpression) + changeLog.changeSets.reverse(true) + skip.toInteger().times { changeLog.changeSets.remove(0) } + + ChangeLogIterator logIterator = new ChangeLogIterator(changeLog, + new ContextChangeSetFilter(contexts), + new DbmsChangeSetFilter(database), + new CountChangeSetFilter(count.toInteger())) + + logIterator.run(new NoopVisitor(database), new RuntimeEnvironment(database, contexts, labelExpression)) + + output.flush() + } finally { + try { + lockService.releaseLock() + executorService.setExecutor('jdbc', database, oldTemplate) + } catch (LockException e) { + throw new LiquibaseException(e.message, e.cause) + } + } + } + + void outputHeader(Executor executor, String message, Liquibase liquibase, Database database) throws DatabaseException { + executor.comment('*********************************************************************') + executor.comment(message) + executor.comment('*********************************************************************') + executor.comment('Change Log: ' + liquibase.changeLogFile) + executor.comment('Ran at: ' + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(new Date())) + DatabaseConnection connection = liquibase.getDatabase().getConnection() + if (connection != null) { + executor.comment('Against: ' + connection.getConnectionUserName() + '@' + connection.getURL()) + } + executor.comment('Liquibase version: ' + LiquibaseUtil.getBuildVersion()) + executor.comment('*********************************************************************' + StreamUtil.getLineSeparator()) + + if (database instanceof OracleDatabase) { + executor.execute(new RawSqlStatement('SET DEFINE OFF;')) + } + if (database instanceof MSSQLDatabase && database.getDefaultCatalogName() != null) { + executor.execute(new RawSqlStatement('USE ' + database.escapeObjectName(database.getDefaultCatalogName(), Catalog) + ';')) + } + } + + private DiffOutputControl createDiffOutputControl() { + def diffOutputControl = new DiffOutputControl(false, false, false) + + String excludeObjects = config.getProperty("${configPrefix}.excludeObjects".toString(), String) + String includeObjects = config.getProperty("${configPrefix}.includeObjects".toString(), String) + if (excludeObjects && includeObjects) { + throw new DatabaseMigrationException('Cannot specify both excludeObjects and includeObjects') + } + if (excludeObjects) { + diffOutputControl.objectChangeFilter = new StandardObjectChangeFilter(StandardObjectChangeFilter.FilterType.EXCLUDE, excludeObjects) + } + if (includeObjects) { + diffOutputControl.objectChangeFilter = new StandardObjectChangeFilter(StandardObjectChangeFilter.FilterType.INCLUDE, includeObjects) + } + + diffOutputControl + } + + void appendToChangeLog(File srcChangeLogFile, File destChangeLogFile) { + if (!srcChangeLogFile.exists() || srcChangeLogFile == destChangeLogFile) { + return + } + + def relativePath = changeLogLocation.toPath().relativize(destChangeLogFile.toPath()).toString() + def extension = getExtension(srcChangeLogFile.name)?.toLowerCase() + + switch (extension) { + case ['yaml', 'yml']: + srcChangeLogFile << """ + |- include: + | file: ${relativePath} + """.stripMargin().trim() + break + case ['xml']: + def text = srcChangeLogFile.text + if (text =~ ']*/>') { + srcChangeLogFile.write(text.replaceFirst('(]*)/>', "\$1>\n \n")) + } else { + srcChangeLogFile.write(text.replaceFirst('', " \n\$0")) + } + break + case ['groovy']: + def text = srcChangeLogFile.text + srcChangeLogFile.write(text.replaceFirst('}.*$', " include file: '$relativePath'\n\$0")) + break + } + } + + String getConfigPrefix() { + return isDefaultDataSource(dataSource) ? + 'grails.plugin.databasemigration' : "grails.plugin.databasemigration.${dataSource}" + } + + private String getExtension(String fileName) { + String extension = '' + + int i = fileName.lastIndexOf('.') + if (i > 0) { + extension = fileName.substring(i + 1) + } + extension + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DbmChangelogToGroovy.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DbmChangelogToGroovy.groovy new file mode 100644 index 00000000000..532ca699bcb --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DbmChangelogToGroovy.groovy @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType + +import liquibase.parser.ChangeLogParserFactory +import liquibase.serializer.ChangeLogSerializerFactory + +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmChangelogToGroovy implements ScriptDatabaseMigrationCommand { + + @Override + void handle() { + def srcFilename = args[0] + if (!srcFilename) { + throw new DatabaseMigrationException("The $name command requires a source filename") + } + + def resourceAccessor = createResourceAccessor() + + def parser = ChangeLogParserFactory.instance.getParser(srcFilename, resourceAccessor) + def databaseChangeLog = parser.parse(srcFilename, null, resourceAccessor) + + def destFilename = args[1] + def destChangeLogFile = resolveChangeLogFile(destFilename) + if (destChangeLogFile) { + if (!destChangeLogFile.path.endsWith('.groovy')) { + throw new DatabaseMigrationException("Destination ChangeLogFile ${destChangeLogFile} must be a Groovy file") + } + if (destChangeLogFile.exists()) { + if (hasOption('force')) { + destChangeLogFile.delete() + } else { + throw new DatabaseMigrationException("ChangeLogFile ${destChangeLogFile} already exists!") + } + } + } + + def serializer = ChangeLogSerializerFactory.instance.getSerializer('groovy') + withFileOrSystemOutputStream(destChangeLogFile) { OutputStream out -> + serializer.write(databaseChangeLog.changeSets, out) + } + + if (destChangeLogFile && hasOption('add')) { + appendToChangeLog(changeLogFile, destChangeLogFile) + } + } + + private static void withFileOrSystemOutputStream(File file, @ClosureParams(value = SimpleType, options = 'java.io.OutputStream') Closure closure) { + if (!file) { + closure.call(System.out) + return + } + + if (!file.parentFile.exists()) { + file.parentFile.mkdirs() + } + file.withOutputStream { OutputStream out -> + closure.call(out) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DbmCreateChangelog.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DbmCreateChangelog.groovy new file mode 100644 index 00000000000..dddc41bfa67 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/DbmCreateChangelog.groovy @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.serializer.ChangeLogSerializer +import liquibase.serializer.ChangeLogSerializerFactory + +import org.grails.plugins.databasemigration.DatabaseMigrationException + +@CompileStatic +class DbmCreateChangelog implements ScriptDatabaseMigrationCommand { + + @Override + void handle() { + def filename = args[0] + if (!filename) { + throw new DatabaseMigrationException("The $name command requires a filename") + } + + def outputChangeLogFile = resolveChangeLogFile(filename) + if (outputChangeLogFile.exists()) { + if (hasOption('force')) { + outputChangeLogFile.delete() + } else { + throw new DatabaseMigrationException("ChangeLogFile ${outputChangeLogFile} already exists!") + } + } + if (!outputChangeLogFile.parentFile.exists()) { + outputChangeLogFile.parentFile.mkdirs() + } + + ChangeLogSerializer serializer = ChangeLogSerializerFactory.instance.getSerializer(filename) + + outputChangeLogFile.withOutputStream { OutputStream out -> + serializer.write([], out) + } + + if (hasOption('add')) { + appendToChangeLog(changeLogFile, outputChangeLogFile) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/ScriptDatabaseMigrationCommand.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/ScriptDatabaseMigrationCommand.groovy new file mode 100644 index 00000000000..0be43b3e95f --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/command/ScriptDatabaseMigrationCommand.groovy @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.transform.CompileStatic + +import liquibase.parser.ChangeLogParser +import liquibase.parser.ChangeLogParserFactory + +import grails.config.ConfigMap +import grails.util.Environment +import grails.util.GrailsNameUtils +import org.grails.cli.profile.ExecutionContext +import org.grails.config.CodeGenConfig +import org.grails.plugins.databasemigration.EnvironmentAwareCodeGenConfig +import org.grails.plugins.databasemigration.liquibase.GroovyChangeLogParser + +import static org.grails.plugins.databasemigration.PluginConstants.DEFAULT_DATASOURCE_NAME + +@CompileStatic +trait ScriptDatabaseMigrationCommand implements DatabaseMigrationCommand { + + ConfigMap config + ConfigMap sourceConfig + ExecutionContext executionContext + + void handle(ExecutionContext executionContext) { + this.executionContext = executionContext + setConfig(executionContext.config) + + this.commandLine = executionContext.commandLine + this.contexts = optionValue('contexts') + this.defaultSchema = optionValue('defaultSchema') + this.dataSource = optionValue('dataSource') ?: DEFAULT_DATASOURCE_NAME + + configureLiquibase() + handle() + } + + void configureLiquibase() { + GroovyChangeLogParser groovyChangeLogParser = ChangeLogParserFactory.instance.parsers.find { ChangeLogParser changeLogParser -> changeLogParser instanceof GroovyChangeLogParser } as GroovyChangeLogParser + groovyChangeLogParser.config = config + } + + abstract void handle() + + String getName() { + return GrailsNameUtils.getScriptName(GrailsNameUtils.getLogicalName(getClass().getName(), 'Command')) + } + + void setConfig(ConfigMap config) { + this.sourceConfig = config + this.config = new EnvironmentAwareCodeGenConfig(sourceConfig as CodeGenConfig, Environment.current.name) + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/ChangelogXml2Groovy.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/ChangelogXml2Groovy.groovy new file mode 100644 index 00000000000..3e92e62d034 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/ChangelogXml2Groovy.groovy @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import groovy.transform.CompileStatic +import groovy.xml.XmlParser + +/** + * Generates a Groovy DSL version of a Liquibase XML changelog. + * + * @author Burt Beckwith + * @author Kazuki YAMAMOTO + */ +@CompileStatic +class ChangelogXml2Groovy { + + protected static final String NEWLINE = System.getProperty('line.separator') + + /** + * Convert a Liquibase XML changelog to Groovy DSL format. + * @param xml the XML + * @return DSL format + */ + static String convert(String xml) { + def groovy = new StringBuilder('databaseChangeLog = {') + groovy.append(NEWLINE) + + new XmlParser(false, false).parseText(xml).each { Node node -> + convertNode(node, groovy, 1) + } + groovy.append('}') + groovy.append(NEWLINE) + groovy.toString() + } + + protected static void convertNode(Node node, StringBuilder groovy, int indentLevel) { + + groovy.append(NEWLINE) + appendWithIndent(indentLevel, groovy, (String) node.name()) + + String mixedText + def children = [] + for (child in node.children()) { + if (child instanceof String) { + mixedText = child + } else { + children << child + } + } + + appendAttrs(groovy, node, mixedText) + + if (children) { + groovy.append(' {') + for (child in children) { + convertNode((Node) child, groovy, indentLevel + 1) + } + appendWithIndent(indentLevel, groovy, '}') + groovy.append(NEWLINE) + } else { + groovy.append(NEWLINE) + } + } + + protected static void appendAttrs(StringBuilder groovy, Node node, String text) { + def local = new StringBuilder() + + String delimiter = '' + + if (text) { + local.append('"""') + local.append(text.replaceAll(/(\$|\\)/, /\\$1/)) + local.append('"""') + delimiter = ', ' + } + + node.attributes().each { name, value -> + local.append(delimiter) + local.append(name) + local.append(': "').append(((String) value).replaceAll(/(\$|\\|\\n)/, /\\$1/)).append('"') + delimiter = ', ' + } + + if (local.length()) { + groovy.append('(') + groovy.append(local.toString()) + groovy.append(')') + } + } + + protected static void appendWithIndent(int indentLevel, StringBuilder groovy, String s) { + indentLevel.times { groovy.append(' ') } + groovy.append(s) + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/DatabaseChangeLogBuilder.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/DatabaseChangeLogBuilder.groovy new file mode 100644 index 00000000000..ad420e35bad --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/DatabaseChangeLogBuilder.groovy @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import groovy.transform.CompileStatic +import org.codehaus.groovy.runtime.InvokerHelper + +import liquibase.parser.core.ParsedNode + +import org.springframework.context.ApplicationContext + +import org.grails.plugins.databasemigration.DatabaseMigrationException + +import static org.grails.plugins.databasemigration.PluginConstants.DATA_SOURCE_NAME_KEY + +@CompileStatic +class DatabaseChangeLogBuilder extends BuilderSupport { + + ApplicationContext applicationContext + + String dataSourceName + + @Override + protected void setParent(Object parent, Object child) { + } + + @Override + protected Object createNode(Object name) { + def node = new ParsedNode(null, (String) name) + if (name == 'grailsChange' || name == 'grailsPrecondition') { + node.addChild(null, 'applicationContext', applicationContext) + node.addChild(null, DATA_SOURCE_NAME_KEY, dataSourceName) + } + if (currentNode) { + currentNode.addChild(node) + } + node + } + + @Override + protected Object createNode(Object name, Object value) { + def node = new ParsedNode(null, (String) name) + node.value = value + if (currentNode) { + currentNode.addChild(node) + } + node + } + + @Override + protected Object createNode(Object name, Map attributes) { + def node = new ParsedNode(null, (String) name) + attributes.each { Object key, Object value -> + node.addChild(null, (String) key, value) + } + currentNode.addChild(node) + node + } + + @Override + protected Object createNode(Object name, Map attributes, Object value) { + def node = new ParsedNode(null, (String) name) + attributes.each { Object key, Object attrValue -> + node.addChild(null, (String) key, attrValue) + } + node.value = value + currentNode.addChild(node) + node + } + + private ParsedNode getCurrentNode() { + (ParsedNode) current + } + + @Override + Object invokeMethod(String methodName, Object args) { + if (currentNode?.name == 'grailsChange') { + processGrailsChangeProperty(methodName, args) + return null + } else if (currentNode?.name == 'grailsPrecondition') { + processGrailsPreconditionProperty(methodName, args) + return null + } else { + return super.invokeMethod(methodName, args) + } + } + + protected void processGrailsChangeProperty(String methodName, Object args) { + def name = methodName.toLowerCase() + def arg = InvokerHelper.asList(args)[0] + if (name == 'init' && arg instanceof Closure) { + currentNode.addChild(null, 'init', arg) + } else if (name == 'validate' && arg instanceof Closure) { + currentNode.addChild(null, 'validate', arg) + } else if (name == 'change' && arg instanceof Closure) { + currentNode.addChild(null, 'change', arg) + } else if (name == 'rollback' && arg instanceof Closure) { + currentNode.addChild(null, 'rollback', arg) + } else if (name == 'confirm' && arg instanceof CharSequence) { + currentNode.addChild(null, 'confirm', arg) + } else if (name == 'checksum' && arg instanceof CharSequence) { + currentNode.addChild(null, 'checksum', arg) + } else { + throw new DatabaseMigrationException("Unknown method name: ${methodName}") + } + } + + protected boolean processGrailsPreconditionProperty(String methodName, args) { + def name = methodName.toLowerCase() + def arg = InvokerHelper.asList(args)[0] + if (name == 'check' && arg instanceof Closure) { + currentNode.addChild(null, 'check', arg) + } else { + throw new DatabaseMigrationException("Unknown method name: ${methodName}") + } + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/EmbeddedJarPathHandler.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/EmbeddedJarPathHandler.groovy new file mode 100644 index 00000000000..bfba3ce565f --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/EmbeddedJarPathHandler.groovy @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.plugins.databasemigration.liquibase + +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.Paths + +import groovy.transform.CompileStatic + +import liquibase.resource.AbstractPathResourceAccessor +import liquibase.resource.PathResource +import liquibase.resource.Resource +import liquibase.resource.ResourceAccessor +import liquibase.resource.ZipPathHandler + +@CompileStatic +class EmbeddedJarPathHandler extends ZipPathHandler { + + @Override + int getPriority(String root) { + if (root.startsWith('jar:file:') && root.endsWith('!/')) { //only can handle `jar:` urls for the entire jar + if (parseJarPath(root).contains('!')) { + return PRIORITY_SPECIALIZED + } + } + PRIORITY_NOT_APPLICABLE + } + + private String parseJarPath(String root) { + root.substring(9, root.lastIndexOf('!')) + } + + @Override + ResourceAccessor getResourceAccessor(String root) throws FileNotFoundException { + String jarPath = parseJarPath(root) + new EmbeddedJarResourceAccessor(jarPath.split('!').toList()) + } +} + +@CompileStatic +class EmbeddedJarResourceAccessor extends AbstractPathResourceAccessor { + + private FileSystem fileSystem + + EmbeddedJarResourceAccessor(List jarPaths) { + try { + Path firstPath = Paths.get(jarPaths.pop()) + fileSystem = FileSystems.newFileSystem(firstPath, null as ClassLoader) + + while (jarPaths) { + Path innerPath = fileSystem.getPath(jarPaths.pop()) + fileSystem = FileSystems.newFileSystem(innerPath, null as ClassLoader) + } + } catch (e) { + throw new IllegalArgumentException(e.getMessage(), e) + } + } + + @Override + void close() throws Exception { + //can't close the filesystem because they often get reused and/or are being used by other things + } + + @Override + protected Path getRootPath() { + return this.fileSystem.getPath('/') + } + + @Override + protected Resource createResource(Path file, String pathToAdd) { + return new PathResource(pathToAdd, file) + } + + @Override + List describeLocations() { + return Collections.singletonList(fileSystem.toString()) + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabase.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabase.groovy new file mode 100644 index 00000000000..b1315943bb8 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabase.groovy @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import groovy.transform.CompileStatic + +import liquibase.database.DatabaseConnection +import liquibase.database.OfflineConnection +import liquibase.exception.DatabaseException +import liquibase.ext.hibernate.database.HibernateDatabase +import liquibase.snapshot.DatabaseSnapshot +import liquibase.snapshot.JdbcDatabaseSnapshot +import liquibase.snapshot.SnapshotControl +import liquibase.structure.DatabaseObject +import org.hibernate.boot.Metadata +import org.hibernate.boot.MetadataSources +import org.hibernate.dialect.Dialect +import org.hibernate.service.ServiceRegistry + +import org.grails.orm.hibernate.HibernateDatastore + +@CompileStatic +class GormDatabase extends HibernateDatabase { + + final String shortName = 'GORM' + final String DefaultDatabaseProductName = 'getDefaultDatabaseProductName' + + private Dialect dialect + private Metadata metadata + DatabaseConnection connection + + GormDatabase() { + } + + GormDatabase(Dialect dialect, ServiceRegistry serviceRegistry, HibernateDatastore hibernateDatastore) { + this.dialect = dialect + this.metadata = hibernateDatastore.getMetadata() + SnapshotControl snapshotControl = new SnapshotControl(this, null, null) + GormDatabase database = this + OfflineConnection connection = new OfflineConnection('offline:gorm', null) { + DatabaseSnapshot getSnapshot(DatabaseObject[] examples) { + new JdbcDatabaseSnapshot(examples, database, snapshotControl) + } + } + this.connection = connection + } + + @Override + Dialect getDialect() { + dialect + } + + /** + * Return the hibernate {@link Metadata} used by this database. + */ + @Override + Metadata getMetadata() { + metadata + } + + @Override + protected void configureSources(MetadataSources sources) throws DatabaseException { + //no op + } + + @Override + boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException { + return false + } + +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibase.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibase.groovy new file mode 100644 index 00000000000..94412c49ee2 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibase.groovy @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import java.sql.Connection + +import groovy.transform.CompileStatic + +import liquibase.Liquibase +import liquibase.database.Database +import liquibase.exception.DatabaseException +import liquibase.exception.LiquibaseException +import liquibase.integration.spring.SpringLiquibase +import liquibase.resource.ResourceAccessor + +import org.springframework.context.ApplicationContext +import org.springframework.core.io.DefaultResourceLoader + +import static org.grails.plugins.databasemigration.PluginConstants.DATA_SOURCE_NAME_KEY + +@CompileStatic +class GrailsLiquibase extends SpringLiquibase { + + private ApplicationContext applicationContext + + String dataSourceName + + String databaseChangeLogTableName + + String databaseChangeLogLockTableName + + GrailsLiquibase(ApplicationContext applicationContext) { + this.applicationContext = applicationContext + this.resourceLoader = new DefaultResourceLoader() + } + + @Override + protected Liquibase createLiquibase(Connection connection) throws LiquibaseException { + Liquibase liquibase = new Liquibase(getChangeLog(), createResourceOpener(), createDatabase(connection, null)) + if (parameters != null) { + for (Map.Entry entry : parameters.entrySet()) { + liquibase.setChangeLogParameter(entry.getKey(), entry.getValue()) + } + } + liquibase.setChangeLogParameter(DATA_SOURCE_NAME_KEY, dataSourceName) + if (isDropFirst()) { + liquibase.dropAll() + } + + return liquibase + } + + @Override + protected Database createDatabase(Connection connection, ResourceAccessor accessor) throws DatabaseException { + Database database = super.createDatabase(connection, accessor) + + if (databaseChangeLogTableName) { + database.databaseChangeLogTableName = databaseChangeLogTableName + } + if (databaseChangeLogLockTableName) { + database.databaseChangeLogLockTableName = databaseChangeLogLockTableName + } + + database + } + + @Override + protected void performUpdate(Liquibase liquibase) throws LiquibaseException { + if (!applicationContext.containsBean('migrationCallbacks')) { + super.performUpdate(liquibase) + return + } + + def database = liquibase.database + def migrationCallbacks = applicationContext.getBean('migrationCallbacks') + + if (migrationCallbacks.metaClass.respondsTo(migrationCallbacks, 'beforeStartMigration')) { + migrationCallbacks.invokeMethod('beforeStartMigration', [database] as Object[]) + } + if (migrationCallbacks.metaClass.respondsTo(migrationCallbacks, 'onStartMigration')) { + migrationCallbacks.invokeMethod('onStartMigration', [database, liquibase, changeLog] as Object[]) + } + + super.performUpdate(liquibase) + + if (migrationCallbacks.metaClass.respondsTo(migrationCallbacks, 'afterMigrations')) { + migrationCallbacks.invokeMethod('afterMigrations', [database] as Object[]) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibaseFactory.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibaseFactory.groovy new file mode 100644 index 00000000000..ee397d01345 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GrailsLiquibaseFactory.groovy @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.plugins.databasemigration.liquibase + +import org.springframework.beans.factory.config.AbstractFactoryBean +import org.springframework.context.ApplicationContext + +class GrailsLiquibaseFactory extends AbstractFactoryBean { + + private final ApplicationContext applicationContext + + GrailsLiquibaseFactory(ApplicationContext applicationContext) { + setSingleton(false) + this.applicationContext = applicationContext + } + + @Override + Class getObjectType() { + return GrailsLiquibase + } + + @Override + protected GrailsLiquibase createInstance() throws Exception { + return new GrailsLiquibase(applicationContext) + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChange.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChange.groovy new file mode 100644 index 00000000000..d57b4fcdd3b --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChange.groovy @@ -0,0 +1,332 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import java.sql.Connection + +import groovy.sql.Sql +import groovy.transform.CompileStatic + +import liquibase.Scope +import liquibase.change.AbstractChange +import liquibase.change.ChangeMetaData +import liquibase.change.CheckSum +import liquibase.change.DatabaseChange +import liquibase.database.Database +import liquibase.database.DatabaseConnection +import liquibase.database.jvm.JdbcConnection +import liquibase.exception.RollbackImpossibleException +import liquibase.exception.SetupException +import liquibase.exception.ValidationErrors +import liquibase.exception.Warnings +import liquibase.executor.ExecutorService +import liquibase.executor.LoggingExecutor +import liquibase.parser.core.ParsedNode +import liquibase.parser.core.ParsedNodeException +import liquibase.resource.ResourceAccessor +import liquibase.statement.SqlStatement + +import org.springframework.context.ApplicationContext + +import grails.config.Config +import grails.core.GrailsApplication +import org.grails.plugins.databasemigration.DatabaseMigrationTransactionManager + +import static org.grails.plugins.databasemigration.PluginConstants.DATA_SOURCE_NAME_KEY + +/** + * Custom Groovy-based change. + * + * @author Burt Beckwith + * @author Kazuki YAMAMOTO + */ +@CompileStatic +@DatabaseChange(name = 'grailsChange', description = 'Executes groovy code to apply a database change.', priority = ChangeMetaData.PRIORITY_DEFAULT) +class GroovyChange extends AbstractChange { + + ApplicationContext ctx + + String dataSourceName + + Closure initClosure + + Closure validateClosure + + Closure changeClosure + + Closure rollbackClosure + + String confirmationMessage + + String checksumString + + Database database + + Sql sql + + ValidationErrors validationErrors = new ValidationErrors() + + Warnings warnings = new Warnings() + + List allStatements = [] + + boolean initClosureCalled + + boolean validateClosureCalled + + boolean changeClosureCalled + + @Override + void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException { + ctx = parsedNode.getChildValue(null, 'applicationContext', ApplicationContext) + dataSourceName = parsedNode.getChildValue(null, DATA_SOURCE_NAME_KEY, String) + if (dataSourceName?.startsWith('dataSource_')) { + dataSourceName = dataSourceName.substring('dataSource_'.length()) + } + + initClosure = parsedNode.getChildValue(null, 'init', Closure) + initClosure?.setResolveStrategy(Closure.DELEGATE_FIRST) + + validateClosure = parsedNode.getChildValue(null, 'validate', Closure) + validateClosure?.setResolveStrategy(Closure.DELEGATE_FIRST) + + changeClosure = parsedNode.getChildValue(null, 'change', Closure) + changeClosure?.setResolveStrategy(Closure.DELEGATE_FIRST) + + rollbackClosure = parsedNode.getChildValue(null, 'rollback', Closure) + rollbackClosure?.setResolveStrategy(Closure.DELEGATE_FIRST) + + confirmationMessage = parsedNode.getChildValue(null, 'confirm', String) + checksumString = parsedNode.getChildValue(null, 'checksum', String) + } + + @Override + void finishInitialization() throws SetupException { + if (!initClosure || initClosureCalled) { + return + } + + initClosure.delegate = this + try { + initClosure() + } catch (Exception e) { + throw new SetupException(e) + } finally { + initClosureCalled = true + } + } + + @Override + ValidationErrors validate(Database database) { + this.database = database + + if (!validateClosure || validateClosureCalled || !shouldRun()) { + return validationErrors + } + + validateClosure.delegate = this + try { + validateClosure() + } finally { + validateClosureCalled = true + } + + return validationErrors + } + + @Override + Warnings warn(Database database) { + validate(database) + warnings + } + + @Override + SqlStatement[] generateStatements(Database database) { + this.database = database + + if (shouldRun() && changeClosure) { + changeClosure.delegate = this + try { + if (!changeClosureCalled) { + withNewTransaction(changeClosure) + } + } finally { + changeClosureCalled = true + } + } + + allStatements as SqlStatement[] + } + + @Override + SqlStatement[] generateRollbackStatements(Database database) throws RollbackImpossibleException { + this.database = database + + if (shouldRun() && rollbackClosure) { + rollbackClosure.delegate = this + rollbackClosure() + } + + allStatements as SqlStatement[] + } + + @Override + String getConfirmationMessage() { + confirmationMessage ?: 'Executed GrailsChange' + } + + @Override + CheckSum generateCheckSum() { + CheckSum.compute(checksumString ?: 'Grails Change') + } + + @Override + boolean supportsRollback(Database database) { + this.database = database + shouldRun() + } + + /** + * Called by the validate closure. Adds a validation error. + * + * @param message the error message + */ + void error(String message) { + validationErrors.addError(message) + } + + /** + * Called by the validate closure. Adds a warning message. + * + * @param warning the warning message + */ + void warn(String warning) { + warnings.addWarning(warning) + } + + /** + * Called by the change or rollback closure. Adds a statement to be executed. + * + * @param statement the statement + */ + void sqlStatement(SqlStatement statement) { + if (statement) { + allStatements << statement + } + } + + /** + * Called by the change or rollback closure. Adds multiple statements to be executed. + * + * @param statement the statement + */ + void sqlStatements(List statements) { + if (statements) { + allStatements.addAll(statements as List) + } + } + + /** + * Called by the change or rollback closure. Overrides the confirmation message. + * + * @param message the confirmation message + */ + void confirm(String message) { + confirmationMessage = message + } + + /** + * Called from the change or rollback closure. Creates a Sql instance from the current connection. + * + * @return the sql instance + */ + Sql getSql() { + if (!connection) { + return null + } + + if (!sql) { + sql = new Sql(connection) { + protected void closeResources(Connection c) { + // do nothing, let Liquibase close the connection + } + } + } + + sql + } + + /** + * Called from the change or rollback closure. Shortcut to get the (wrapper) database connection. + * + * @return the connection or null if the database isn't set yet + */ + DatabaseConnection getDatabaseConnection() { + database?.connection + } + + /** + * Called from the change or rollback closure. Shortcut to get the real database connection. + * + * @return the connection or null if the database isn't set yet + */ + Connection getConnection() { + if (databaseConnection instanceof JdbcConnection) { + return ((JdbcConnection) database.connection).underlyingConnection + } + return null + } + + /** + * Called from the change or rollback closure. Shortcut for the current application. + * + * @return the application + */ + GrailsApplication getApplication() { + ctx.getBean(GrailsApplication) + } + + /** + * Called from the change or rollback closure. Shortcut for the current config. + * + * @return the config + */ + Config getConfig() { + application.config + } + + /** + * + * @return Whether the database executor is instance of LoggingExecutor + */ + protected boolean shouldRun() { + !(Scope.getCurrentScope().getSingleton(ExecutorService).getExecutor('jdbc', database) instanceof LoggingExecutor) + } + + /** + * Executes the grailsChange>change block within the context of a new transaction + * + * @param callable The changeClosure to call + * @return The result of the closure execution + */ + protected void withNewTransaction(Closure callable) { + new DatabaseMigrationTransactionManager(ctx, dataSourceName) + .withNewTransaction(callable) + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogParser.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogParser.groovy new file mode 100644 index 00000000000..0083c7aa800 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogParser.groovy @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.codehaus.groovy.control.CompilerConfiguration + +import liquibase.changelog.ChangeLogParameters +import liquibase.exception.ChangeLogParseException +import liquibase.parser.core.ParsedNode +import liquibase.parser.core.xml.AbstractChangeLogParser +import liquibase.resource.ResourceAccessor + +import org.springframework.context.ApplicationContext + +import grails.config.ConfigMap +import grails.io.IOUtils + +import static org.grails.plugins.databasemigration.PluginConstants.DATA_SOURCE_NAME_KEY + +@CompileStatic +class GroovyChangeLogParser extends AbstractChangeLogParser { + + final int priority = PRIORITY_DEFAULT + + ApplicationContext applicationContext + + ConfigMap config + + @Override + @CompileDynamic + protected ParsedNode parseToNode(String physicalChangeLogLocation, ChangeLogParameters changeLogParameters, ResourceAccessor resourceAccessor) throws ChangeLogParseException { + def inputStream = null + def changeLogText = null + try { + inputStream = resourceAccessor.openStreams(null, physicalChangeLogLocation).first() + changeLogText = inputStream?.text + } finally { + IOUtils.closeQuietly(inputStream) + } + + CompilerConfiguration compilerConfiguration = new CompilerConfiguration(CompilerConfiguration.DEFAULT) + if (compilerConfiguration.metaClass.respondsTo(compilerConfiguration, 'setDisabledGlobalASTTransformations')) { + Set disabled = compilerConfiguration.disabledGlobalASTTransformations ?: [] + disabled << 'org.grails.datastore.gorm.query.transform.GlobalDetachedCriteriaASTTransformation' + compilerConfiguration.disabledGlobalASTTransformations = disabled + } + + def changeLogProperties = config.getProperty('changelogProperties', Map) ?: [:] + + try { + GroovyClassLoader classLoader = new GroovyClassLoader(Thread.currentThread().contextClassLoader, compilerConfiguration, false) + Script script = new GroovyShell(classLoader, new Binding(changeLogProperties), compilerConfiguration).parse(changeLogText as String) + script.run() + + setChangeLogProperties(changeLogProperties, changeLogParameters) + + Closure databaseChangeLogBlock = script.getProperty('databaseChangeLog') as Closure + + DatabaseChangeLogBuilder builder = new DatabaseChangeLogBuilder() + builder.dataSourceName = changeLogParameters.getValue(DATA_SOURCE_NAME_KEY, null) + builder.applicationContext = applicationContext + builder.databaseChangeLog(databaseChangeLogBlock) as ParsedNode + } catch (Exception e) { + throw new ChangeLogParseException(e) + } + } + + @Override + boolean supports(String changeLogFile, ResourceAccessor resourceAccessor) { + changeLogFile.endsWith('.groovy') + } + + @CompileDynamic + protected void setChangeLogProperties(Map changeLogProperties, ChangeLogParameters changeLogParameters) { + changeLogProperties.each { name, value -> + String contexts = null + String labels = null + String databases = null + if (value instanceof Map) { + contexts = value.contexts + labels = value.labels + databases = value.databases + value = value.value + } + changeLogParameters.set(name as String, value as String, contexts as String, labels, databases, true, null) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogSerializer.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogSerializer.groovy new file mode 100644 index 00000000000..162fe79fa59 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogSerializer.groovy @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import groovy.transform.CompileStatic + +import liquibase.changelog.ChangeLogChild +import liquibase.changelog.ChangeSet +import liquibase.serializer.ChangeLogSerializer +import liquibase.serializer.LiquibaseSerializable +import liquibase.serializer.core.xml.XMLChangeLogSerializer + +@CompileStatic +class GroovyChangeLogSerializer implements ChangeLogSerializer { + + private XMLChangeLogSerializer xmlChangeLogSerializer = new XMLChangeLogSerializer() + + @Override + def void write(List changesets, OutputStream out) throws IOException { + def xmlOutputStrem = new ByteArrayOutputStream() + xmlChangeLogSerializer.write(changesets, xmlOutputStrem) + out << ChangelogXml2Groovy.convert(xmlOutputStrem.toString()) + } + + @Override + void append(ChangeSet changeSet, File changeLogFile) throws IOException { + throw new UnsupportedOperationException() + } + + @Override + String[] getValidFileExtensions() { + ['groovy'] as String[] + } + + @Override + String serialize(LiquibaseSerializable object, boolean pretty) { + throw new UnsupportedOperationException() + } + + @Override + int getPriority() { + return 0 + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyDiffToChangeLogCommandStep.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyDiffToChangeLogCommandStep.groovy new file mode 100644 index 00000000000..a226fa5c29d --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyDiffToChangeLogCommandStep.groovy @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import groovy.transform.CompileStatic + +import liquibase.command.CommandResultsBuilder +import liquibase.command.CommandScope +import liquibase.command.core.DiffChangelogCommandStep +import liquibase.command.core.DiffCommandStep +import liquibase.command.core.InternalSnapshotCommandStep +import liquibase.command.core.helpers.DiffOutputControlCommandStep +import liquibase.command.core.helpers.ReferenceDbUrlConnectionCommandStep +import liquibase.database.Database +import liquibase.database.ObjectQuotingStrategy +import liquibase.diff.DiffResult +import liquibase.diff.output.DiffOutputControl +import liquibase.serializer.ChangeLogSerializerFactory + +import grails.util.GrailsStringUtils + +@CompileStatic +class GroovyDiffToChangeLogCommandStep extends DiffChangelogCommandStep { + + public static final String[] COMMAND_NAME = new String[] {'groovyDiffChangelog'} + + @Override + void run(CommandResultsBuilder resultsBuilder) { + CommandScope commandScope = resultsBuilder.getCommandScope() + Database referenceDatabase = commandScope.getArgumentValue(ReferenceDbUrlConnectionCommandStep.REFERENCE_DATABASE_ARG) + String changeLogFile = commandScope.getArgumentValue(CHANGELOG_FILE_ARG) + + InternalSnapshotCommandStep.logUnsupportedDatabase(referenceDatabase, this.getClass()) + + DiffCommandStep diffCommandStep = new DiffCommandStep() + + DiffResult diffResult = diffCommandStep.createDiffResult(resultsBuilder) + + PrintStream outputStream = new PrintStream(resultsBuilder.getOutputStream()) + + ObjectQuotingStrategy originalStrategy = referenceDatabase.getObjectQuotingStrategy() + + DiffOutputControl diffOutputControl = (DiffOutputControl) resultsBuilder.getResult(DiffOutputControlCommandStep.DIFF_OUTPUT_CONTROL.getName()) + + try { + referenceDatabase.setObjectQuotingStrategy(ObjectQuotingStrategy.QUOTE_ALL_OBJECTS) + if (GrailsStringUtils.trimToNull(changeLogFile) == null) { + createDiffToChangeLogObject(diffResult, diffOutputControl, false).print(outputStream, ChangeLogSerializerFactory.instance.getSerializer('groovy')) + } else { + createDiffToChangeLogObject(diffResult, diffOutputControl, false).print(changeLogFile, ChangeLogSerializerFactory.instance.getSerializer(changeLogFile)) + } + } + finally { + referenceDatabase.setObjectQuotingStrategy(originalStrategy) + outputStream.flush() + } + resultsBuilder.addResult('statusCode', 0) + + } + + @Override + String[][] defineCommandNames() { + return new String[][] { COMMAND_NAME } + } + +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyGenerateChangeLogCommandStep.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyGenerateChangeLogCommandStep.groovy new file mode 100644 index 00000000000..bff305a1b00 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyGenerateChangeLogCommandStep.groovy @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import groovy.transform.CompileStatic + +import liquibase.Scope +import liquibase.command.CommandResultsBuilder +import liquibase.command.CommandScope +import liquibase.command.core.DiffCommandStep +import liquibase.command.core.GenerateChangelogCommandStep +import liquibase.command.core.InternalSnapshotCommandStep +import liquibase.command.core.helpers.DiffOutputControlCommandStep +import liquibase.command.core.helpers.ReferenceDbUrlConnectionCommandStep +import liquibase.database.Database +import liquibase.database.ObjectQuotingStrategy +import liquibase.diff.DiffResult +import liquibase.diff.output.DiffOutputControl +import liquibase.diff.output.changelog.DiffToChangeLog +import liquibase.serializer.ChangeLogSerializerFactory + +import grails.util.GrailsStringUtils + +@CompileStatic +class GroovyGenerateChangeLogCommandStep extends GenerateChangelogCommandStep { + + public static final String[] COMMAND_NAME = new String[] {'groovyGenerateChangeLog'} + + private static final String INFO_MESSAGE = + 'When generating formatted SQL changelogs, it is important to decide if batched statements\n' + + "should be split or not. For storedlogic objects, the default behavior is 'splitStatements:false'\n." + + "All other objects default to 'splitStatements:true'. See https://docs.liquibase.org for additional information." + + @Override + void run(CommandResultsBuilder resultsBuilder) throws Exception { + CommandScope commandScope = resultsBuilder.getCommandScope() + + String changeLogFile = GrailsStringUtils.trimToNull(commandScope.getArgumentValue(CHANGELOG_FILE_ARG)) + if (changeLogFile != null && changeLogFile.toLowerCase().endsWith('.sql')) { + Scope.getCurrentScope().getUI().sendMessage('\n' + INFO_MESSAGE + '\n') + Scope.getCurrentScope().getLog(getClass()).info('\n' + INFO_MESSAGE + '\n') + } + + final Database referenceDatabase = commandScope.getArgumentValue(ReferenceDbUrlConnectionCommandStep.REFERENCE_DATABASE_ARG) + + InternalSnapshotCommandStep.logUnsupportedDatabase(referenceDatabase, this.getClass()) + + DiffCommandStep diffCommandStep = new DiffCommandStep() + + DiffResult diffResult = diffCommandStep.createDiffResult(resultsBuilder) + + DiffOutputControl diffOutputControl = (DiffOutputControl) resultsBuilder.getResult(DiffOutputControlCommandStep.DIFF_OUTPUT_CONTROL.getName()) + + DiffToChangeLog changeLogWriter = new DiffToChangeLog(diffResult, diffOutputControl) + + changeLogWriter.setChangeSetAuthor(commandScope.getArgumentValue(AUTHOR_ARG)) + changeLogWriter.setChangeSetContext(commandScope.getArgumentValue(CONTEXT_ARG)) + changeLogWriter.setChangeSetPath(changeLogFile) + + ObjectQuotingStrategy originalStrategy = referenceDatabase.getObjectQuotingStrategy() + try { + referenceDatabase.setObjectQuotingStrategy(ObjectQuotingStrategy.QUOTE_ALL_OBJECTS) + if (GrailsStringUtils.trimToNull(changeLogFile) != null) { + changeLogWriter.print(changeLogFile, ChangeLogSerializerFactory.instance.getSerializer(changeLogFile)) + } else { + PrintStream outputStream = new PrintStream(resultsBuilder.getOutputStream()) + try { + changeLogWriter.print(outputStream, ChangeLogSerializerFactory.instance.getSerializer('groovy')) + } finally { + outputStream.flush() + } + + } + if (GrailsStringUtils.trimToNull(changeLogFile) != null) { + Scope.getCurrentScope().getUI().sendMessage('Generated changelog written to ' + new File(changeLogFile).getAbsolutePath()) + } + } finally { + referenceDatabase.setObjectQuotingStrategy(originalStrategy) + } + } + + @Override + String[][] defineCommandNames() { + return new String[][] { COMMAND_NAME } + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyPrecondition.groovy b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyPrecondition.groovy new file mode 100644 index 00000000000..5abcb6b467b --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/groovy/org/grails/plugins/databasemigration/liquibase/GroovyPrecondition.groovy @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import java.sql.Connection + +import groovy.sql.Sql +import groovy.transform.CompileStatic + +import liquibase.CatalogAndSchema +import liquibase.changelog.ChangeSet +import liquibase.changelog.DatabaseChangeLog +import liquibase.changelog.visitor.ChangeExecListener +import liquibase.database.Database +import liquibase.database.DatabaseConnection +import liquibase.database.jvm.JdbcConnection +import liquibase.exception.DatabaseException +import liquibase.exception.PreconditionErrorException +import liquibase.exception.PreconditionFailedException +import liquibase.exception.ValidationErrors +import liquibase.exception.Warnings +import liquibase.parser.core.ParsedNode +import liquibase.parser.core.ParsedNodeException +import liquibase.precondition.AbstractPrecondition +import liquibase.resource.ResourceAccessor +import liquibase.snapshot.DatabaseSnapshot +import liquibase.snapshot.SnapshotControl +import liquibase.snapshot.SnapshotGeneratorFactory + +import org.springframework.context.ApplicationContext + +import grails.config.Config +import grails.core.GrailsApplication + +/** + * Custom Groovy-based precondition. + * + * @author Burt Beckwith + * @author Kazuki YAMAMOTO + */ +@CompileStatic +class GroovyPrecondition extends AbstractPrecondition { + + final String serializedObjectNamespace = STANDARD_CHANGELOG_NAMESPACE + + final String name = 'grailsPrecondition' + + Closure checkClosure + + Database database + + DatabaseChangeLog changeLog + + ChangeSet changeSet + + ResourceAccessor resourceAccessor + + ApplicationContext ctx + + Sql sql + + @Override + void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException { + this.resourceAccessor = resourceAccessor + + ctx = parsedNode.getChildValue(null, 'applicationContext', ApplicationContext) + checkClosure = parsedNode.getChildValue(null, 'check', Closure) + checkClosure?.setResolveStrategy(Closure.DELEGATE_FIRST) + } + + @Override + Warnings warn(Database database) { + new Warnings() + } + + @Override + ValidationErrors validate(Database database) { + new ValidationErrors() + } + + @Override + void check(Database database, DatabaseChangeLog changeLog, ChangeSet changeSet, ChangeExecListener changeExecListener) throws PreconditionFailedException, PreconditionErrorException { + this.database = database + this.changeLog = changeLog + this.changeSet = changeSet + + if (!checkClosure) { + return + } + + checkClosure.delegate = this + + try { + checkClosure() + } catch (PreconditionFailedException e) { + throw e + } catch (AssertionError e) { + throw new PreconditionFailedException(e.message, changeLog, this) + } catch (Exception e) { + throw new PreconditionErrorException(e, changeLog, this) + } + } + + /** + * Called from the change or rollback closure. Creates a Sql instance from the current connection. + * + * @return the sql instance + */ + Sql getSql() { + if (!connection) { + return null + } + + if (!sql) { + sql = new Sql(connection) { + protected void closeResources(Connection c) { + // do nothing, let Liquibase close the connection + } + } + } + + sql + } + + /** + * Called from the change or rollback closure. Shortcut to get the (wrapper) database connection. + * + * @return the connection or null if the database isn't set yet + */ + DatabaseConnection getDatabaseConnection() { + database?.connection + } + + /** + * Called from the change or rollback closure. Shortcut to get the real database connection. + * + * @return the connection or null if the database isn't set yet + */ + Connection getConnection() { + if (databaseConnection instanceof JdbcConnection) { + return ((JdbcConnection) database.connection).underlyingConnection + } + return null + } + + /** + * Called from the change or rollback closure. Shortcut for the current application. + * + * @return the application + */ + GrailsApplication getApplication() { + ctx.getBean(GrailsApplication) + } + + /** + * Called from the change or rollback closure. Shortcut for the current config. + * + * @return the config + */ + Config getConfig() { + application.config + } + + /** + * Called from the check closure as a shortcut to throw a PreconditionFailedException. + * + * @param message the failure message + */ + void fail(String message) { + throw new PreconditionFailedException(message, changeLog, this) + } + + /** + * Called from the check closure. + * + * @param schemaName the schema name + * @return a snapshot for the current database and schema name + */ + DatabaseSnapshot createDatabaseSnapshot(String schemaName = null) { + try { + return SnapshotGeneratorFactory.instance.createSnapshot(new CatalogAndSchema(null, schemaName), database, new SnapshotControl(database)) + } catch (DatabaseException e) { + throw new PreconditionErrorException(e, changeLog, this) + } + } +} diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.change.Change b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.change.Change new file mode 100644 index 00000000000..28032a072dd --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.change.Change @@ -0,0 +1 @@ +org.grails.plugins.databasemigration.liquibase.GroovyChange \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.command.CommandStep b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.command.CommandStep new file mode 100644 index 00000000000..57807cf02f8 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.command.CommandStep @@ -0,0 +1,2 @@ +org.grails.plugins.databasemigration.liquibase.GroovyDiffToChangeLogCommandStep +org.grails.plugins.databasemigration.liquibase.GroovyGenerateChangeLogCommandStep \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.database.Database b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.database.Database new file mode 100644 index 00000000000..ac4dce94511 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.database.Database @@ -0,0 +1 @@ +org.grails.plugins.databasemigration.liquibase.GormDatabase \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.parser.ChangeLogParser b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.parser.ChangeLogParser new file mode 100644 index 00000000000..34140176c3e --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.parser.ChangeLogParser @@ -0,0 +1 @@ +org.grails.plugins.databasemigration.liquibase.GroovyChangeLogParser \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.precondition.Precondition b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.precondition.Precondition new file mode 100644 index 00000000000..fef65c4e6ce --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.precondition.Precondition @@ -0,0 +1 @@ +org.grails.plugins.databasemigration.liquibase.GroovyPrecondition \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.resource.PathHandler b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.resource.PathHandler new file mode 100644 index 00000000000..4675c5fdc95 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.resource.PathHandler @@ -0,0 +1 @@ +org.grails.plugins.databasemigration.liquibase.EmbeddedJarPathHandler \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.serializer.ChangeLogSerializer b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.serializer.ChangeLogSerializer new file mode 100644 index 00000000000..ea14cdd7d56 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/services/liquibase.serializer.ChangeLogSerializer @@ -0,0 +1 @@ +org.grails.plugins.databasemigration.liquibase.GroovyChangeLogSerializer \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..302af8c095f --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,106 @@ +{ + "groups": [ + { + "name": "grails.plugin.databasemigration", + "description": "Database Migration Plugin" + }, + { + "name": "grails.plugin.databasemigration.startup", + "description": "Database Migration Plugin - Startup" + } + ], + "properties": [ + { + "name": "grails.plugin.databasemigration.updateOnStart", + "type": "java.lang.Boolean", + "description": "Whether to run changesets from the specified file at application startup.", + "defaultValue": false + }, + { + "name": "grails.plugin.databasemigration.updateAllOnStart", + "type": "java.lang.Boolean", + "description": "Whether to run changesets at startup for all configured datasources.", + "defaultValue": false + }, + { + "name": "grails.plugin.databasemigration.updateOnStartFileName", + "type": "java.lang.String", + "description": "The changelog file name to run at startup. For named datasources uses `changelog-.groovy`.", + "defaultValue": "changelog.groovy" + }, + { + "name": "grails.plugin.databasemigration.dropOnStart", + "type": "java.lang.Boolean", + "description": "Whether to drop all database tables before auto-running migrations at startup.", + "defaultValue": false + }, + { + "name": "grails.plugin.databasemigration.updateOnStartContexts", + "type": "java.util.List", + "description": "Liquibase contexts to activate when running migrations at startup. Empty means all contexts.", + "defaultValue": [] + }, + { + "name": "grails.plugin.databasemigration.updateOnStartLabels", + "type": "java.util.List", + "description": "Liquibase labels to filter changesets when running migrations at startup. Empty means all labels.", + "defaultValue": [] + }, + { + "name": "grails.plugin.databasemigration.updateOnStartDefaultSchema", + "type": "java.lang.String", + "description": "The default database schema to use when running migrations at startup.", + "defaultValue": null + }, + { + "name": "grails.plugin.databasemigration.databaseChangeLogTableName", + "type": "java.lang.String", + "description": "The name of the Liquibase changelog tracking table.", + "defaultValue": "DATABASECHANGELOG" + }, + { + "name": "grails.plugin.databasemigration.databaseChangeLogLockTableName", + "type": "java.lang.String", + "description": "The name of the Liquibase lock table used to prevent concurrent migrations.", + "defaultValue": "DATABASECHANGELOGLOCK" + }, + { + "name": "grails.plugin.databasemigration.changelogLocation", + "type": "java.lang.String", + "description": "The directory containing changelog files.", + "defaultValue": "grails-app/migrations" + }, + { + "name": "grails.plugin.databasemigration.changelogFileName", + "type": "java.lang.String", + "description": "The name of the main changelog file. For named datasources uses `changelog-.groovy`.", + "defaultValue": "changelog.groovy" + }, + { + "name": "grails.plugin.databasemigration.contexts", + "type": "java.lang.String", + "description": "Comma-delimited list of Liquibase contexts to use for all operations.", + "defaultValue": null + }, + { + "name": "grails.plugin.databasemigration.excludeObjects", + "type": "java.lang.String", + "description": "Comma-delimited list of database objects to ignore in diff and generate operations.", + "defaultValue": null + }, + { + "name": "grails.plugin.databasemigration.includeObjects", + "type": "java.lang.String", + "description": "Comma-delimited list of database objects to include in diff and generate operations (all others excluded).", + "defaultValue": null + }, + { + "name": "grails.plugin.databasemigration.skipUpdateOnStartMainClasses", + "type": "java.util.List", + "description": "List of main class names for which startup migrations are skipped, preventing migrations when running CLI commands.", + "defaultValue": [ + "grails.ui.command.GrailsApplicationContextCommandRunner" + ] + } + ] +} diff --git a/grails-data-hibernate7/dbmigration/src/main/resources/migration.gdsl b/grails-data-hibernate7/dbmigration/src/main/resources/migration.gdsl new file mode 100644 index 00000000000..f08aad3f274 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/resources/migration.gdsl @@ -0,0 +1,686 @@ +migrationDir = ".*/grails-app/migrations/.*" + +final String STRING = String.name + +contributor(context(pathRegexp: migrationDir, scope: scriptScope())) { + property(name: "databaseChangeLog", type: {}) +} + +def changelogBody = context(scope: closureScope()) +contributor([changelogBody]) { + method(name: "changeSet", params: [ + args: [ + parameter(name: 'id', type: STRING), + parameter(name: 'author', type: STRING), + parameter(name: 'dbms', type: STRING), + parameter(name: 'runAlways', type: STRING), + parameter(name: 'runOnChange', type: STRING), + parameter(name: 'context', type: STRING), + parameter(name: 'runInTransaction', type: STRING), + parameter(name: 'failOnError', type: STRING), + parameter(name: 'description', type: STRING) + ], + body: {} + + ], type: void) + + method(name: "include", params: [ + args: [parameter(name: 'file', type: STRING)] + ], type: void) +} + +void provideChildsOf(String parentMethod, Closure callback) { + provideChildsOf(parentMethod, true, callback) +} + +void provideChildsOf(String parentMethod, boolean isArg, Closure callback) { + def c = context(scope: closureScope(isArg: isArg), pathRegexp: migrationDir) + + contributor([c]) { + if (enclosingCall(parentMethod)) { + Closure cloned = callback.clone() + cloned.delegate = delegate + cloned.call() + } + } +} + +//Grails changes +provideChildsOf("changeSet") { + method(name: "grailsChange", params: [:], body: {}) +} + +provideChildsOf("grailsChange") { + method(name: "init", params: [:], body: {}, type: void) + method(name: "validate", params: [:], body: {}, type: void) + method(name: "change", params: [:], body: {}, type: void) + method(name: "rollback", params: [:], body: {}, type: void) + method(name: "confirm", params: [:], body: {}, type: void) + method(name: "checkSum", params: [:], body: {}, type: void) +} + +provideChildsOf("change") { + property(name: "changeSet", type: "liquibase.changelog.ChangeSet") + property(name: "resourceAccessor", type: "liquibase.resource.ResourceAccessor") + property(name: "ctx", type: "org.springframework.context.ApplicationContext") + property(name: "application", type: "org.codehaus.groovy.grails.commons.GrailsApplication") + property(name: "database", type: "liquibase.database.Database") + property(name: "databaseConnection", type: "liquibase.database.DatabaseConnection") + property(name: "connection", type: "java.sql.Connection") + property(name: "sql", type: "groovy.sql.Sql") +} + +provideChildsOf("rollback") { + property(name: "database", type: "liquibase.database.Database") + property(name: "databaseConnection", type: "liquibase.database.DatabaseConnection") + property(name: "connection", type: "java.sql.Connection") + property(name: "sql", type: "groovy.sql.Sql") +} + + +provideChildsOf("changeSet") { + method(name: "sql", params: [query: "java.lang.String"], type: void) +} + +provideChildsOf("changeSet") { + method(name: "sqlFile", params: [ + args: [ + parameter(name: "dbms", type: STRING), + parameter(name: "encoding", type: STRING), + parameter(name: "endDelimiter", type: STRING), + parameter(name: "path", type: STRING), + parameter(name: "relativeToChangelogFile", type: STRING), + parameter(name: "splitStatements", type: STRING), + parameter(name: "stripComments", type: STRING), + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "createSequence", params: [ + args: [ + parameter(name: "catalogName", type: STRING), + parameter(name: "cycle", type: STRING), + parameter(name: "incrementBy", type: STRING), + parameter(name: "maxValue", type: STRING), + parameter(name: "minValue", type: STRING), + parameter(name: "ordered", type: STRING), + parameter(name: "schemaName", type: STRING), + parameter(name: "sequenceName", type: STRING), + parameter(name: "startValue", type: STRING) + ] + ], type: void) +} + +//liquibase changes +List columnArgs = [ + parameter(name: 'name', type: STRING), + parameter(name: 'type', type: STRING), + parameter(name: 'value', type: STRING), + parameter(name: 'valueNumeric', type: STRING), + parameter(name: 'valueBoolean', type: STRING), + parameter(name: 'valueDate', type: STRING), + parameter(name: 'defaultValue', type: STRING), + parameter(name: 'defaultValueNumeric', type: STRING), + parameter(name: 'defaultValueBoolean', type: STRING), + parameter(name: 'defaultValueDate', type: STRING), + parameter(name: 'autoIncrement', type: STRING), + parameter(name: 'beforeColumn', type: STRING), + parameter(name: 'afterColumn', type: STRING), + parameter(name: 'position', type: STRING), + parameter(name: 'descending', type: STRING), + +] + +columnType = { + method(name: "column", params: [ + args: columnArgs, + body: {} + ], type: void) +} + +columnTypeNoBody = { + method(name: "column", params: [ + args: columnArgs + ], type: void) +} + +List param = [ + parameter(name: 'name', type: STRING), + parameter(name: 'value', type: STRING) +] + +provideChildsOf("changeSet") { + method(name: "createTable", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'remarks', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING), + parameter(name: 'tablespace', type: STRING), + ], + body: {} + ], type: void) + +} +provideChildsOf("createTable", columnType) +provideChildsOf("createTable", columnTypeNoBody) + +provideChildsOf("changeSet") { + method(name: "createView", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'replaceIfExists', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'selectQuery', type: STRING), + parameter(name: 'viewName', type: STRING) + ] + ], type: void) + +} + +provideChildsOf("column") { + method(name: "constraints", params: [ + args: [ + parameter(name: 'nullable', type: STRING), + parameter(name: 'primaryKey', type: STRING), + parameter(name: 'primaryKeyName', type: STRING), + parameter(name: 'unique', type: STRING), + parameter(name: 'uniqueConstraintName', type: STRING), + parameter(name: 'references', type: STRING), + parameter(name: 'foreignKeyName', type: STRING), + parameter(name: 'deleteCascade', type: STRING), + parameter(name: 'deferrable', type: STRING), + parameter(name: 'initiallyDeferred', type: STRING) + ] + ], type: void) +} + + +provideChildsOf("changeSet") { + method(name: "addAutoIncrement", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnDataType', type: STRING), + parameter(name: 'columnName', type: STRING), + parameter(name: 'incrementBy', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'startWith', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) + +} + +provideChildsOf("changeSet") { + method(name: "addColumn", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ], + body: {} + ], type: void) +} +provideChildsOf("addColumn", columnType) +provideChildsOf("addColumn", columnTypeNoBody) + +provideChildsOf("changeSet") { + method(name: "addDefaultValue", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnDataType', type: STRING), + parameter(name: 'columnName', type: STRING), + parameter(name: 'defaultValue', type: STRING), + parameter(name: 'defaultValueBoolean', type: STRING), + parameter(name: 'defaultValueComputed', type: STRING), + parameter(name: 'defaultValueDate', type: STRING), + parameter(name: 'defaultValueNumeric', type: STRING), + parameter(name: 'defaultValueSequenceNext', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "addForeignKeyConstraint", params: [ + args: [ + parameter(name: 'baseColumnNames', type: STRING), + parameter(name: 'baseTableCatalogName', type: STRING), + parameter(name: 'baseTableName', type: STRING), + parameter(name: 'baseTableSchemaName', type: STRING), + parameter(name: 'constraintName', type: STRING), + parameter(name: 'deferrable', type: STRING), + parameter(name: 'initiallyDeferred', type: STRING), + parameter(name: 'onDelete', type: STRING), + parameter(name: 'onUpdate', type: STRING), + parameter(name: 'referencedColumnNames', type: STRING), + parameter(name: 'referencedTableCatalogName', type: STRING), + parameter(name: 'referencedTableName', type: STRING), + parameter(name: 'referencedTableSchemaName', type: STRING), + parameter(name: 'referencesUniqueColumn', type: STRING), + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "addNotNullConstraint", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnDataType', type: STRING), + parameter(name: 'columnName', type: STRING), + parameter(name: 'defaultNullValue', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "addPrimaryKey", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnNames', type: STRING), + parameter(name: 'constraintName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING), + parameter(name: 'tablespace', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "addUniqueConstraint", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnNames', type: STRING), + parameter(name: 'constraintName', type: STRING), + parameter(name: 'deferrable', type: STRING), + parameter(name: 'disabled', type: STRING), + parameter(name: 'initiallyDeferred', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING), + parameter(name: 'tablespace', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "createIndex", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'indexName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING), + parameter(name: 'tablespace', type: STRING), + parameter(name: 'unique', type: STRING), + ], + body: {} + ], type: void) +} +provideChildsOf("createIndex", columnType) +provideChildsOf("createIndex", columnTypeNoBody) + +provideChildsOf("changeSet") { + method(name: "delete", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING), + parameter(name: 'where', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropAllForeignKeyConstraints", params: [ + args: [ + parameter(name: 'baseTableCatalogName', type: STRING), + parameter(name: 'baseTableName', type: STRING), + parameter(name: 'baseTableSchemaName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropColumn", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropDefaultValue", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnDataType', type: STRING), + parameter(name: 'columnName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropForeignKeyConstraint", params: [ + args: [ + parameter(name: 'baseTableCatalogName', type: STRING), + parameter(name: 'baseTableName', type: STRING), + parameter(name: 'baseTableSchemaName', type: STRING), + parameter(name: 'constraintName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropIndex", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'indexName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropNotNullConstraint", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnDataType', type: STRING), + parameter(name: 'columnName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropPrimaryKey", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'constraintName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropTable", params: [ + args: [ + parameter(name: 'cascadeConstraints', type: STRING), + parameter(name: 'catalogName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropUniqueConstraint", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'constraintName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING), + parameter(name: 'uniqueColumns', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropView", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'viewName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "modifyDataType", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnName', type: STRING), + parameter(name: 'newDataType', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "renameColumn", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'columnDataType', type: STRING), + parameter(name: 'newColumnName', type: STRING), + parameter(name: 'oldColumnName', type: STRING), + parameter(name: 'remarks', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "renameTable", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'newTableName', type: STRING), + parameter(name: 'oldTableName', type: STRING), + parameter(name: 'schemaName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "renameView", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'newViewName', type: STRING), + parameter(name: 'oldViewName', type: STRING), + parameter(name: 'schemaName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "addLookupTable", params: [ + args: [ + parameter(name: 'constraintName', type: STRING), + parameter(name: 'existingColumnName', type: STRING), + parameter(name: 'existingTableCatalogName', type: STRING), + parameter(name: 'existingTableName', type: STRING), + parameter(name: 'existingTableSchemaName', type: STRING), + parameter(name: 'newColumnDataType', type: STRING), + parameter(name: 'newColumnName', type: STRING), + parameter(name: 'newTableCatalogName', type: STRING), + parameter(name: 'newTableName', type: STRING), + parameter(name: 'newTableSchemaName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "alterSequence", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'incrementBy', type: STRING), + parameter(name: 'maxValue', type: STRING), + parameter(name: 'minValue', type: STRING), + parameter(name: 'ordered', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'sequenceName', type: STRING), + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "createProcedure", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'comments', type: STRING), + parameter(name: 'dbms', type: STRING), + parameter(name: 'encoding', type: STRING), + parameter(name: 'path', type: STRING), + parameter(name: 'procedureName', type: STRING), + parameter(name: 'procedureText', type: STRING), + parameter(name: 'relativeToChangelogFile', type: STRING), + parameter(name: 'schemaName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "customChange", params: [ + args: [ + parameter(name: 'class', type: STRING), + ], + body: {} + ], type: void) +} + +provideChildsOf("customChange") { + method(name: "param", params: [ + args: [ + parameter(name: 'id', type: STRING), + parameter(name: 'value', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropProcedure", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'procedureName', type: STRING), + parameter(name: 'schemaName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "dropSequence", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'sequenceName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "empty", params: [], type: void) +} + +provideChildsOf("changeSet") { + method(name: "executeCommand", params: [ + args: [ + parameter(name: 'executable', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "insert", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'dbms', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ], + body: {} + ], type: void) +} +provideChildsOf("insert", columnType) +provideChildsOf("insert", columnTypeNoBody) + +provideChildsOf("changeSet") { + method(name: "loadData", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'encoding', type: STRING), + parameter(name: 'file', type: STRING), + parameter(name: 'quotchar', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'separator', type: STRING), + parameter(name: 'tableName', type: STRING) + ], + body: {} + ], type: void) +} +provideChildsOf("loadData", columnType) +provideChildsOf("loadData", columnTypeNoBody) + +provideChildsOf("changeSet") { + method(name: "loadUpdateData", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'encoding', type: STRING), + parameter(name: 'file', type: STRING), + parameter(name: 'primaryKey', type: STRING), + parameter(name: 'quotchar', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'separator', type: STRING), + parameter(name: 'tableName', type: STRING) + ], + body: {} + ], type: void) +} +provideChildsOf("loadUpdateData", columnType) +provideChildsOf("loadUpdateData", columnTypeNoBody) + +provideChildsOf("changeSet") { + method(name: "mergeColumns", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'column1Name', type: STRING), + parameter(name: 'column2Name', type: STRING), + parameter(name: 'finalColumnName', type: STRING), + parameter(name: 'finalColumnType', type: STRING), + parameter(name: 'joinString', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "stop", params: [ + args: [ + parameter(name: 'message', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "tagDatabase", params: [ + args: [ + parameter(name: 'tag', type: STRING) + ] + ], type: void) +} + +provideChildsOf("changeSet") { + method(name: "update", params: [ + args: [ + parameter(name: 'catalogName', type: STRING), + parameter(name: 'schemaName', type: STRING), + parameter(name: 'tableName', type: STRING), + parameter(name: 'where', type: STRING) + ], + body: {} + ], type: void) +} +provideChildsOf("update", columnType) +provideChildsOf("update", columnTypeNoBody) \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/main/scripts/dbm-changelog-to-groovy.groovy b/grails-data-hibernate7/dbmigration/src/main/scripts/dbm-changelog-to-groovy.groovy new file mode 100644 index 00000000000..44bd0a7082e --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/scripts/dbm-changelog-to-groovy.groovy @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.grails.plugins.databasemigration.DatabaseMigrationException +import org.grails.plugins.databasemigration.command.DbmChangelogToGroovy + +description('Converts a changelog file to a Groovy DSL file') { + usage 'grails [environment] dbm-changelog-to-groovy [src_file_name] [dest_file_name]' + flag name: 'src_file_name', description: 'The name and path of the changelog file to convert' + flag name: 'dest_file_name', description: 'The name and path of the Groovy file' + flag name: 'dataSource', description: 'if provided will run the script for the specified dataSource creating a file named changelog-dataSource.groovy if a filename is not given. Not needed for the default dataSource' + flag name: 'force', description: 'Whether to overwrite existing files' + flag name: 'add', description: 'if provided will run the script for the specified dataSource. Not needed for the default dataSource.' +} + +try { + new DbmChangelogToGroovy().handle(executionContext) +} catch (DatabaseMigrationException e) { + error e.message, e +} diff --git a/grails-data-hibernate7/dbmigration/src/main/scripts/dbm-create-changelog.groovy b/grails-data-hibernate7/dbmigration/src/main/scripts/dbm-create-changelog.groovy new file mode 100644 index 00000000000..accff252dbe --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/main/scripts/dbm-create-changelog.groovy @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.grails.plugins.databasemigration.DatabaseMigrationException +import org.grails.plugins.databasemigration.command.DbmCreateChangelog + +description('Creates an empty changelog file') { + usage 'grails [environment] dbm-create-changelog [filename]' + flag name: 'filename', description: 'The path to the output file to write to' + flag name: 'dataSource', description: 'if provided will run the script for the specified dataSource creating a file named changelog-dataSource.groovy if a filename is not given. Not needed for the default dataSource' + flag name: 'force', description: 'Whether to overwrite existing files' + flag name: 'add', description: 'if provided will run the script for the specified dataSource. Not needed for the default dataSource.' +} + +try { + new DbmCreateChangelog().handle(executionContext) +} catch (DatabaseMigrationException e) { + error e.message, e +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommandSpec.groovy new file mode 100644 index 00000000000..9199398b00e --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ApplicationContextDatabaseMigrationCommandSpec.groovy @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.plugins.databasemigration.command + +import grails.config.Config +import grails.core.DefaultGrailsApplication +import grails.core.GrailsApplication +import grails.core.support.GrailsApplicationAware +import grails.dev.commands.ApplicationCommand +import grails.dev.commands.ExecutionContext +import grails.orm.bootstrap.HibernateDatastoreSpringInitializer +import grails.persistence.Entity +import grails.util.GrailsNameUtils +import liquibase.parser.ChangeLogParser +import liquibase.parser.ChangeLogParserFactory +import org.grails.build.parsing.CommandLineParser +import org.grails.config.PropertySourcesConfig +import org.grails.plugins.databasemigration.liquibase.GroovyChangeLogParser +import org.h2.Driver +import org.springframework.context.support.GenericApplicationContext +import org.springframework.core.env.MapPropertySource +import org.springframework.core.env.MutablePropertySources +import spock.lang.AutoCleanup + +abstract class ApplicationContextDatabaseMigrationCommandSpec extends DatabaseMigrationCommandSpec implements GrailsApplicationAware { + + GrailsApplication grailsApplication + + @AutoCleanup + GenericApplicationContext applicationContext + + ApplicationCommand command + + Config config + + def setup() { + applicationContext = new GenericApplicationContext() + + applicationContext.beanFactory.registerSingleton('dataSource', dataSource) + applicationContext.beanFactory.registerSingleton(GrailsApplication.APPLICATION_ID, new DefaultGrailsApplication()) + + def mutablePropertySources = new MutablePropertySources() + mutablePropertySources.addFirst(new MapPropertySource('TestConfig', [ + 'grails.plugin.databasemigration.changelogLocation': changeLogLocation.canonicalPath, + 'dataSource.dbCreate' : '', + 'environments.test.dataSource.url' : 'jdbc:h2:mem:testDb', + 'dataSource.username' : 'sa', + 'dataSource.password' : '', + 'dataSource.driverClassName' : Driver.name, + 'environments.other.dataSource.url' : 'jdbc:h2:mem:otherDb', + ])) + config = new PropertySourcesConfig(mutablePropertySources) + + def datastoreInitializer = new HibernateDatastoreSpringInitializer(config, domainClasses) + datastoreInitializer.configureForBeanDefinitionRegistry(applicationContext) + + applicationContext.refresh() + + def grailsApplication = applicationContext.getBean(GrailsApplication) + grailsApplication.config = config + + def groovyChangeLogParser = ChangeLogParserFactory.instance.parsers.find { ChangeLogParser changeLogParser -> changeLogParser instanceof GroovyChangeLogParser } as GroovyChangeLogParser + groovyChangeLogParser.applicationContext = applicationContext + groovyChangeLogParser.config = config + + if (commandClass != null) { + command = createCommand(commandClass) + } + } + + protected ApplicationCommand createCommand(Class applicationCommand) { + def command = applicationCommand.getDeclaredConstructor().newInstance() + command.applicationContext = applicationContext + command.changeLogFile.parentFile.mkdirs() + return command + } + + protected Class[] getDomainClasses() { + [] as Class[] + } + + protected Class getCommandClass() { + null + } + + protected ExecutionContext getExecutionContext(Class clazz = commandClass, String... args) { + def commandClassName = GrailsNameUtils.getScriptName(GrailsNameUtils.getLogicalName(clazz.name, 'Command')) + new ExecutionContext( + new CommandLineParser().parse(([commandClassName] + args.toList()) as String[]) + ) + } + + void cleanup() { + + } + + +} + +@Entity +class Book { + String title + Author author +} + +@Entity +class Author { + String name + static hasMany = [books: Book] +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommandConfigSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommandConfigSpec.groovy new file mode 100644 index 00000000000..5a0f6baa5db --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommandConfigSpec.groovy @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import org.h2.Driver +import spock.lang.Specification +import org.grails.testing.GrailsUnitTest + +class DatabaseMigrationCommandConfigSpec extends Specification implements DatabaseMigrationCommand, GrailsUnitTest { + + void cleanup() { + config.remove('dataSource') + config.remove('dataSources') + } + + void "test getDataSourceConfig with single dataSource"() { + + when: + config.dataSource = [ + 'dbCreate' : '', + 'url' : 'jdbc:h2:mem:testDb', + 'username' : 'sa', + 'password' : '', + 'driverClassName': Driver.name + ] + + then: + getDataSourceConfig(config) == [ + 'dbCreate' : '', + 'url' : 'jdbc:h2:mem:testDb', + 'username' : 'sa', + 'password' : '', + 'driverClassName': Driver.name + ] + + } + + void "test getDataSourceConfig with no dataSource config"() { + expect: + getDataSourceConfig(config) == null + } + + void "test getDataSourceConfig should return config when default is defined in dataSources"() { + when: + config.dataSources = [ + dataSource: [ + 'dbCreate' : '', + 'url' : 'jdbc:h2:mem:testDb', + 'username' : 'sa', + 'password' : '', + 'driverClassName': Driver.name + ] + ] + + then: + getDataSourceConfig(config) == [ + 'dbCreate' : '', + 'url' : 'jdbc:h2:mem:testDb', + 'username' : 'sa', + 'password' : '', + 'driverClassName': Driver.name, + ] + + } + + void "test getDataSourceConfig should return config when both dataSource and dataSources exists"() { + when: + config.dataSource = [ + 'dbCreate' : '', + 'url' : 'jdbc:h2:mem:testDb', + 'username' : 'sa', + 'password' : '', + 'driverClassName': Driver.name + ] + config.dataSources = [ + other: [ + 'dbCreate' : '', + 'url' : 'jdbc:h2:mem:otherDb', + 'username' : 'sa', + 'password' : '', + 'driverClassName': Driver.name + ] + ] + + then: + getDataSourceConfig(config) == [ + 'dbCreate' : '', + 'url' : 'jdbc:h2:mem:testDb', + 'username' : 'sa', + 'password' : '', + 'driverClassName': Driver.name, + ] + + } + +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommandSpec.groovy new file mode 100644 index 00000000000..574b5807ce1 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DatabaseMigrationCommandSpec.groovy @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import groovy.sql.Sql +import org.grails.plugins.databasemigration.testing.annotation.OutputCapture +import org.h2.Driver +import org.springframework.jdbc.datasource.DriverManagerDataSource +import spock.lang.AutoCleanup +import spock.lang.Specification + +import javax.sql.DataSource +import java.sql.Connection + +abstract class DatabaseMigrationCommandSpec extends Specification { + + @OutputCapture Object output + + DataSource dataSource + + @AutoCleanup + Connection connection + + @AutoCleanup + Sql sql + + @AutoCleanup('deleteDir') + File changeLogLocation + + def setup() { + dataSource = new DriverManagerDataSource('jdbc:h2:mem:testDb', 'sa', '') + dataSource.driverClassName = Driver.name + connection = dataSource.connection + sql = new Sql(connection) + + changeLogLocation = File.createTempDir() + } + + + protected static extractOutput(Object output){ + String out = output.toString() + out.getAt(out.indexOf("databaseChangeLog")..-1)?.replaceAll(/\s/,"") + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommandSpec.groovy new file mode 100644 index 00000000000..524e8b2503c --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommandSpec.groovy @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand + +class DbmChangelogSyncCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmChangelogSyncCommand + } + + def "marks all changes as executed in the database"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext()) + + then: + def rows = sql.rows('SELECT id FROM databasechangelog').collect { it.id } + rows == ['changeSet1', 'changeSet2'] + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + changeSet(author: "John Smith", id: "changeSet1") { + } + changeSet(author: "John Smith", id: "changeSet2") { + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommandSqlSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommandSqlSpec.groovy new file mode 100644 index 00000000000..2db88a8ff58 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmChangelogSyncCommandSqlSpec.groovy @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import spock.lang.AutoCleanup + +class DbmChangelogSyncCommandSqlSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmChangelogSyncSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('sync', 'sql') + + def "writes SQL to mark all changes as executed in the database to STDOUT"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext()) + + then: + def outputString = output.toString() + outputString =~ /INSERT INTO .+'changeSet1'/ + outputString =~ /INSERT INTO .+'changeSet2'/ + } + + def "writes SQL to mark all changes as executed in the database to a file given as arguments"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext(outputFile.canonicalPath)) + + then: + def outputString = outputFile.text + outputString =~ /INSERT INTO .+'changeSet1'/ + outputString =~ /INSERT INTO .+'changeSet2'/ + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + changeSet(author: "John Smith", id: "changeSet1") { + } + changeSet(author: "John Smith", id: "changeSet2") { + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmClearChecksumsCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmClearChecksumsCommandSpec.groovy new file mode 100644 index 00000000000..a4e6d04ed8f --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmClearChecksumsCommandSpec.groovy @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand + +class DbmClearChecksumsCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmClearChecksumsCommand + } + + def "removes all saved checksums from database log"() { + given: + sql.executeUpdate ''' +CREATE TABLE DATABASECHANGELOG (ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED TIMESTAMP NOT NULL, ORDEREXECUTED INT NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35), DESCRIPTION VARCHAR(255), COMMENTS VARCHAR(255), TAG VARCHAR(255), LIQUIBASE VARCHAR(20)); +INSERT INTO DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, DESCRIPTION, COMMENTS, EXECTYPE, LIQUIBASE) VALUES ('changeSet1', 'John Smith', 'changelog.yml', NOW(), 1, '7:d41d8cd98f00b204e9800998ecf8427e', 'Empty', '', 'EXECUTED', '3.3.2'); +INSERT INTO DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, DESCRIPTION, COMMENTS, EXECTYPE, LIQUIBASE) VALUES ('changeSet2', 'John Smith', 'changelog.yml', NOW(), 2, '7:d41d8cd98f00b204e9800998ecf8427e', 'Empty', '', 'EXECUTED', '3.3.2'); +''' + + when: + command.handle(getExecutionContext()) + + then: + def rows = sql.rows('select id, md5sum from databasechangelog') + rows.size() == 2 + rows.every { it.md5sum == null } + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDiffCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDiffCommandSpec.groovy new file mode 100644 index 00000000000..043b350d46a --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDiffCommandSpec.groovy @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import groovy.sql.Sql +import org.grails.plugins.databasemigration.DatabaseMigrationException +import org.h2.Driver +import org.springframework.jdbc.datasource.DriverManagerDataSource +import spock.lang.AutoCleanup + +import java.sql.Connection + +class DbmDiffCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmDiffCommand + } + + @AutoCleanup + Connection otherDbConnection + + @AutoCleanup + Sql otherDbSql + + def setup() { + def otherDbDataSource = new DriverManagerDataSource('jdbc:h2:mem:otherDb', 'sa', '') + otherDbDataSource.driverClassName = Driver.name + otherDbConnection = otherDbDataSource.connection + otherDbSql = new Sql(otherDbConnection) + otherDbSql.executeUpdate ''' +CREATE TABLE book (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, title VARCHAR(255) NOT NULL, CONSTRAINT PK_BOOK PRIMARY KEY (id)) +''' + sql.executeUpdate ''' +CREATE TABLE book (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, title VARCHAR(255) NOT NULL, price INT NOT NULL, CONSTRAINT PK_BOOK PRIMARY KEY (id)); +CREATE TABLE author (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT PK_AUTHOR PRIMARY KEY (id)); +''' + } + + def "writes Change Log to update the database to STDOUT"() { + when: + command.handle(getExecutionContext('other')) + + then: + String expected = extractOutput(output) + expected =~ ''' +databaseChangeLog = \\{ + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + createTable\\(tableName: "AUTHOR"\\) \\{ + column\\(autoIncrement: "true", name: "ID", type: "INT"\\) \\{ + constraints\\(nullable:"false", primaryKey: "true", primaryKeyName: "PK_AUTHOR"\\) + \\} + + column\\(name: "NAME", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + addColumn\\(tableName: "BOOK"\\) \\{ + column\\(name: "PRICE", type: "INTEGER"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} +\\} +'''.replaceAll(/\s/,"") + } + + def "writes Change Log to update the database to a file given as arguments"() { + given: + def outputChangeLog = new File(changeLogLocation, 'diff.groovy') + + when: + command.handle(getExecutionContext('other', outputChangeLog.name)) + + then: + outputChangeLog.text?.replaceAll(/\s/,"") =~ ''' +databaseChangeLog = \\{ + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + createTable\\(tableName: "AUTHOR"\\) \\{ + column\\(autoIncrement: "true", name: "ID", type: "INT"\\) \\{ + constraints\\(nullable:"false", primaryKey: "true", primaryKeyName: "PK_AUTHOR"\\) + \\} + + column\\(name: "NAME", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + addColumn\\(tableName: "BOOK"\\) \\{ + column\\(name: "PRICE", type: "INTEGER"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} +\\} +'''.replaceAll(/\s/,"") + } + + def "an error occurs if the otherEnv parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'You must specify the environment to diff against' + } + + def "an error occurs if other environment and current environment is same"() { + when: + command.handle(getExecutionContext('test')) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'You must specify a different environment than the one the command is running in' + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDropAllCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDropAllCommandSpec.groovy new file mode 100644 index 00000000000..7583c4739ff --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmDropAllCommandSpec.groovy @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand + +class DbmDropAllCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmDropAllCommand + } + + def "drops all database objects"() { + given: + sql.executeUpdate 'CREATE TABLE book (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, title VARCHAR(255) NOT NULL, CONSTRAINT PK_BOOK PRIMARY KEY (id))' + + expect: + sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'') + + when: + command.handle(getExecutionContext()) + + then: + !sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'') + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmFutureRollbackCountSqlCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmFutureRollbackCountSqlCommandSpec.groovy new file mode 100644 index 00000000000..9ec47790f3e --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmFutureRollbackCountSqlCommandSpec.groovy @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException +import spock.lang.AutoCleanup + +class DbmFutureRollbackCountSqlCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmFutureRollbackCountSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('rollback', 'sql') + + def "writes SQL to roll back the database to STDOUT"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext('1')) + + then: + def output = output.toString() + output.contains('DROP TABLE PUBLIC.author;') + !output.contains('DROP TABLE PUBLIC.book;') + } + + def "writes SQL to roll back the database to a file given as arguments"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext('1', outputFile.canonicalPath)) + + then: + def output = outputFile.text + output.contains('DROP TABLE PUBLIC.author;') + !output.contains('DROP TABLE PUBLIC.book;') + } + + def "an error occurs if the count parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "The ${command.name} command requires a change set number argument" + } + + def "an error occurs if the count parameter is not number"() { + when: + command.handle(getExecutionContext('one')) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'The change set number argument \'one\' isn\'t a number' + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "3") { + addForeignKeyConstraint(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK_4sac2ubmnqva85r8bk8fxdvbf", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author") + } + + changeSet(author: "John Smith", id: "4", context: "development") { + insert(tableName: "author") { + column(name: "name", value: "Mary") + } + } + + changeSet(author: "John Smith", id: "5", context: "test") { + insert(tableName: "author") { + column(name: "name", value: "Amelia") + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmFutureRollbackSqlCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmFutureRollbackSqlCommandSpec.groovy new file mode 100644 index 00000000000..7e4203b77e1 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmFutureRollbackSqlCommandSpec.groovy @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import spock.lang.AutoCleanup + +class DbmFutureRollbackSqlCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmFutureRollbackSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('rollback', 'sql') + + def "writes SQL to roll back the database to STDOUT"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext()) + + then: + def output = output.toString() + output.contains('ALTER TABLE PUBLIC.book DROP CONSTRAINT FK_4sac2ubmnqva85r8bk8fxdvbf') + output.contains('DROP TABLE PUBLIC.author;') + output.contains('DROP TABLE PUBLIC.book;') + } + + def "writes SQL to roll back the database to a file given as arguments"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext(outputFile.canonicalPath)) + + then: + def output = outputFile.text + output.contains('ALTER TABLE PUBLIC.book DROP CONSTRAINT FK_4sac2ubmnqva85r8bk8fxdvbf') + output.contains('DROP TABLE PUBLIC.author;') + output.contains('DROP TABLE PUBLIC.book;') + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "3") { + addForeignKeyConstraint(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK_4sac2ubmnqva85r8bk8fxdvbf", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author") + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGenerateChangelogCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGenerateChangelogCommandSpec.groovy new file mode 100644 index 00000000000..a8ac019d28c --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGenerateChangelogCommandSpec.groovy @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand + +class DbmGenerateChangelogCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmGenerateChangelogCommand + } + + def setup() { + sql.executeUpdate ''' +CREATE TABLE book (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, title VARCHAR(255) NOT NULL, price INT NOT NULL, CONSTRAINT PK_BOOK PRIMARY KEY (id)); +CREATE TABLE author (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT PK_AUTHOR PRIMARY KEY (id)); +''' + } + + def "generates an initial changelog from the database to STDOUT"() { + when: + command.handle(getExecutionContext()) + + then: + extractOutput(output) =~ ''' +databaseChangeLog = \\{ + + changeSet\\(author: ".*?", id: ".*?"\\) \\{ + createTable\\(tableName: "AUTHOR"\\) \\{ + column\\(autoIncrement: "true", name: "ID", type: "INT"\\) \\{ + constraints\\(nullable: "false", primaryKey: "true", primaryKeyName: "PK_AUTHOR"\\) + \\} + + column\\(name: "NAME", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".*?", id: ".*?"\\) \\{ + createTable\\(tableName: "BOOK"\\) \\{ + column\\(autoIncrement: "true", name: "ID", type: "INT"\\) \\{ + constraints\\(nullable: "false", primaryKey: "true", primaryKeyName: "PK_BOOK"\\) + \\} + + column\\(name: "TITLE", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "PRICE", type: "INT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} +\\} +'''.replaceAll(/\s/, "") + } + + def "generates an initial changelog from the database to a file given as arguments"() { + given: + def outputChangeLog = new File(changeLogLocation, 'changelog.groovy') + + when: + command.handle(getExecutionContext(outputChangeLog.name)) + + then: + outputChangeLog.text?.replaceAll(/\s/, "") =~ ''' +databaseChangeLog = \\{ + + changeSet\\(author: ".*?", id: ".*?"\\) \\{ + createTable\\(tableName: "AUTHOR"\\) \\{ + column\\(autoIncrement: "true", name: "ID", type: "INT"\\) \\{ + constraints\\(nullable: "false", primaryKey: "true", primaryKeyName: "PK_AUTHOR"\\) + \\} + + column\\(name: "NAME", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".*?", id: ".*?"\\) \\{ + createTable\\(tableName: "BOOK"\\) \\{ + column\\(autoIncrement: "true", name: "ID", type: "INT"\\) \\{ + constraints\\(nullable: "false", primaryKey: "true", primaryKeyName: "PK_BOOK"\\) + \\} + + column\\(name: "TITLE", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "PRICE", type: "INT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} +\\} +'''.replaceAll(/\s/, "") + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGenerateGormChangelogCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGenerateGormChangelogCommandSpec.groovy new file mode 100644 index 00000000000..059f07da070 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGenerateGormChangelogCommandSpec.groovy @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +class DbmGenerateGormChangelogCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmGenerateGormChangelogCommand + } + + def "writes Change Log to copy the current state of the database to STDOUT"() { + when: + command.handle(getExecutionContext()) + + then: + extractOutput(output) =~ ''' +databaseChangeLog = \\{ + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + createTable\\(tableName: "author"\\) \\{ + column\\(autoIncrement: "true", name: "id", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false", primaryKey: "true", primaryKeyName: "authorPK"\\) + \\} + + column\\(name: "version", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "name", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + createTable\\(tableName: "book"\\) \\{ + column\\(autoIncrement: "true", name: "id", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false", primaryKey: "true", primaryKeyName: "bookPK"\\) + \\} + + column\\(name: "version", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "title", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "author_id", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + addForeignKeyConstraint\\(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK.+?", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author", validate: "true"\\) + \\} +\\} +'''.replaceAll(/\s/,"") + } + + def "writes Change Log to copy the current state of the database to a file given as arguments"() { + given: + def filename = 'changelog.groovy' + + when: + command.handle(getExecutionContext(filename)) + + then: + def output = new File(changeLogLocation, filename).text?.replaceAll(/\s/, "") + output =~ ''' +databaseChangeLog = \\{ + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + createTable\\(tableName: "author"\\) \\{ + column\\(autoIncrement: "true", name: "id", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false", primaryKey: "true", primaryKeyName: "authorPK"\\) + \\} + + column\\(name: "version", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "name", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + createTable\\(tableName: "book"\\) \\{ + column\\(autoIncrement: "true", name: "id", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false", primaryKey: "true", primaryKeyName: "bookPK"\\) + \\} + + column\\(name: "version", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "title", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "author_id", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + addForeignKeyConstraint\\(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK.+?", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author", validate: "true"\\) + \\} +\\} +'''.replaceAll(/\s/, "") + } + + def "an error occurs if changeLogFile already exists"() { + given: + def filename = 'changelog.yml' + def changeLogFile = new File(changeLogLocation, filename) + assert changeLogFile.createNewFile() + + when: + command.handle(getExecutionContext(filename)) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "ChangeLogFile ${changeLogFile.canonicalPath} already exists!" + } + + @Override + protected Class[] getDomainClasses() { + [Book, Author] as Class[] + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGormDiffCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGormDiffCommandSpec.groovy new file mode 100644 index 00000000000..9d7d3561ae8 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmGormDiffCommandSpec.groovy @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +class DbmGormDiffCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmGormDiffCommand + } + + def setup() { + sql.executeUpdate 'CREATE TABLE PUBLIC.author (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT authorPK PRIMARY KEY (id));' + } + + def "diffs GORM classes against a database and generates a changelog to STDOUT"() { + when: + command.handle(getExecutionContext()) + + then: + extractOutput(output) =~ ''' +databaseChangeLog = \\{ + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + createTable\\(tableName: "book"\\) \\{ + column\\(autoIncrement: "true", name: "id", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false", primaryKey: "true", primaryKeyName: "bookPK"\\) + \\} + + column\\(name: "version", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "title", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "author_id", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + addForeignKeyConstraint\\(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK.+?", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author", validate: "true"\\) + \\} +\\} +'''.replaceAll(/\s/,"") + } + + def "diffs GORM classes against a database and generates a changelog to a file given as arguments"() { + given: + def filename = 'changelog.groovy' + + when: + command.handle(getExecutionContext(filename)) + + then: + def output = new File(changeLogLocation, filename).text?.replaceAll(/\s/,"") + output =~ ''' +databaseChangeLog = \\{ + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + createTable\\(tableName: "book"\\) \\{ + column\\(autoIncrement: "true", name: "id", type: "BIGINT"\\) \\{ + constraints\\(nullable:"false", primaryKey: "true", primaryKeyName: "bookPK"\\) + \\} + + column\\(name: "version", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "title", type: "VARCHAR\\(255\\)"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + + column\\(name: "author_id", type: "BIGINT"\\) \\{ + constraints\\(nullable: "false"\\) + \\} + \\} + \\} + + changeSet\\(author: ".+?", id: ".+?"\\) \\{ + addForeignKeyConstraint\\(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK.+?", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author", validate: "true"\\) + \\} +\\} +'''.replaceAll(/\s/,"") + } + + def "an error occurs if changeLogFile already exists"() { + given: + def filename = 'changelog.yml' + def changeLogFile = new File(changeLogLocation, filename) + assert changeLogFile.createNewFile() + + when: + command.handle(getExecutionContext(filename)) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "ChangeLogFile ${changeLogFile.canonicalPath} already exists!" + } + + @Override + protected Class[] getDomainClasses() { + [Book, Author] as Class[] + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmListLocksCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmListLocksCommandSpec.groovy new file mode 100644 index 00000000000..a58fc104c5e --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmListLocksCommandSpec.groovy @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import spock.lang.AutoCleanup + +class DbmListLocksCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmListLocksCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('locks', 'txt') + + def "lists locks on the database changelog when the lock does not exist"() { + when: + command.handle(getExecutionContext()) + + then: + output.toString().contains '- No locks' + } + + def "lists locks on the database changelog when the lock exists"() { + given: + sql.executeUpdate('CREATE TABLE PUBLIC.DATABASECHANGELOGLOCK (ID INT NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP, LOCKEDBY VARCHAR(255), CONSTRAINT PK_DATABASECHANGELOGLOCK PRIMARY KEY (ID))') + sql.executeUpdate('INSERT INTO PUBLIC.DATABASECHANGELOGLOCK (ID, LOCKED, LOCKGRANTED, LOCKEDBY) VALUES (1, TRUE, NOW(), \'John Smith\')') + + when: + command.handle(getExecutionContext()) + + then: + output.toString() =~ '- John Smith at .+?' + } + + def "lists locks to a file given as arguments"() { + when: + command.handle(getExecutionContext(outputFile.canonicalPath)) + + then: + outputFile.text.contains '- No locks' + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanCommandSpec.groovy new file mode 100644 index 00000000000..717fce94d3b --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanCommandSpec.groovy @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand + +class DbmMarkNextChangesetRanCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmMarkNextChangesetRanCommand + } + + def "marks the next change changes as executed in the database"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext()) + + then: + def rows = sql.rows('SELECT id FROM databasechangelog').collect { it.id } + rows == ['changeSet1'] + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + changeSet(author: "John Smith", id: "changeSet1") { + } + changeSet(author: "John Smith", id: "changeSet2") { + } +} +''' +} + diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanSqlCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanSqlCommandSpec.groovy new file mode 100644 index 00000000000..9056fcb9697 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmMarkNextChangesetRanSqlCommandSpec.groovy @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import spock.lang.AutoCleanup + +class DbmMarkNextChangesetRanSqlCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmMarkNextChangesetRanSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('update', 'sql') + + def "writes SQL to mark the next change as executed in the database to STDOUT"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext()) + + then: + def output = output.toString() + output =~ /INSERT INTO .+'changeSet1'/ + !(output =~ /INSERT INTO .+'changeSet2'/) + } + + def "writes SQL to mark the next change as executed in the database to a file given as arguments"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext(outputFile.canonicalPath)) + + then: + def output = outputFile.text + output =~ /INSERT INTO .+'changeSet1'/ + !(output =~ /INSERT INTO .+'changeSet2'/) + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + changeSet(author: "John Smith", id: "changeSet1") { + } + changeSet(author: "John Smith", id: "changeSet2") { + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmPreviousChangesetSqlCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmPreviousChangesetSqlCommandSpec.groovy new file mode 100644 index 00000000000..48d82511412 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmPreviousChangesetSqlCommandSpec.groovy @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException +import spock.lang.AutoCleanup + +class DbmPreviousChangesetSqlCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmPreviousChangesetSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('previous', 'sql') + + + def setup() { + command.changeLogFile << CHANGE_LOG_CONTENT + + new DbmUpdateCommand(applicationContext: applicationContext).handle(getExecutionContext()) + + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + assert tables as Set == ['book', 'author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + + void "The last SQL change sets to STDOUT"() { + when: + command.handle(getExecutionContext('1')) + + then: + def output = output.toString() + output.contains('CREATE TABLE PUBLIC.book (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, author_id BIGINT NOT NULL, title VARCHAR(255) NOT NULL, CONSTRAINT bookPK PRIMARY KEY (id));') + } + + void "The last SQL change sets to a file given as arguments"() { + when: + command.handle(getExecutionContext('1', outputFile.canonicalPath)) + + then: + def output = outputFile.text + output.contains('CREATE TABLE PUBLIC.book (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, author_id BIGINT NOT NULL, title VARCHAR(255) NOT NULL, CONSTRAINT bookPK PRIMARY KEY (id));') + + } + + void "The second last SQL change sets to a file given as arguments"() { + when: + command.handle(getExecutionContext('1', outputFile.canonicalPath, "--skip=1")) + + then: + def output = outputFile.text + output.contains('CREATE TABLE PUBLIC.author (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT authorPK PRIMARY KEY (id));') + + } + + void "an error occurs if the count parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "The ${command.name} command requires a change set number argument" + } + + void "an error occurs if the count parameter is not number"() { + when: + command.handle(getExecutionContext('one')) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'The change set number argument \'one\' isn\'t a number' + } + + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmReleaseLocksCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmReleaseLocksCommandSpec.groovy new file mode 100644 index 00000000000..75c99838815 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmReleaseLocksCommandSpec.groovy @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand + +class DbmReleaseLocksCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmReleaseLocksCommand + } + + def "releases all locks on the database changelog"() { + given: + sql.executeUpdate('CREATE TABLE PUBLIC.DATABASECHANGELOGLOCK (ID INT NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP, LOCKEDBY VARCHAR(255), CONSTRAINT PK_DATABASECHANGELOGLOCK PRIMARY KEY (ID))') + sql.executeUpdate('INSERT INTO PUBLIC.DATABASECHANGELOGLOCK (ID, LOCKED, LOCKGRANTED, LOCKEDBY) VALUES (1, TRUE, NOW(), \'John Smith\')') + + when: + command.handle(getExecutionContext()) + + then: + sql.rows('SELECT * FROM PUBLIC.DATABASECHANGELOGLOCK ') + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCommandSpec.groovy new file mode 100644 index 00000000000..80de7605b64 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCommandSpec.groovy @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +class DbmRollbackCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmRollbackCommand + } + + def setup() { + command.changeLogFile << CHANGE_LOG_CONTENT + + new DbmUpdateCountCommand(applicationContext: applicationContext).handle(getExecutionContext('1')) + new DbmTagCommand(applicationContext: applicationContext).handle(getExecutionContext('test-tag')) + new DbmUpdateCommand(applicationContext: applicationContext).handle(getExecutionContext()) + + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + assert tables as Set == ['book', 'author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + def "rolls back the database to the state it was in when the tag was applied"() { + when: + command.handle(getExecutionContext('test-tag')) + + then: + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + tables as Set == ['author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + def "an error occurs if tagName parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "The ${command.name} command requires a tag" + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCountCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCountCommandSpec.groovy new file mode 100644 index 00000000000..bdca2008584 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCountCommandSpec.groovy @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +class DbmRollbackCountCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmRollbackCountCommand + } + + def setup() { + command.changeLogFile << CHANGE_LOG_CONTENT + + new DbmUpdateCommand(applicationContext: applicationContext).handle(getExecutionContext()) + + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + assert tables as Set == ['book', 'author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + def "rolls back the specified number of change sets"() { + when: + command.handle(getExecutionContext('1')) + + then: + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + tables as Set == ['author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + def "an error occurs if the count parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "The ${command.name} command requires a change set number argument" + } + + def "an error occurs if the count parameter is not number"() { + when: + command.handle(getExecutionContext('one')) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'The change set number argument \'one\' isn\'t a number' + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCountSqlCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCountSqlCommandSpec.groovy new file mode 100644 index 00000000000..e3a1915b134 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackCountSqlCommandSpec.groovy @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException +import spock.lang.AutoCleanup + +class DbmRollbackCountSqlCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmRollbackCountSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('rollback', 'sql') + + def setup() { + command.changeLogFile << CHANGE_LOG_CONTENT + + new DbmUpdateCommand(applicationContext: applicationContext).handle(getExecutionContext()) + + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + assert tables as Set == ['book', 'author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + def "writes the SQL to roll back the specified number of change sets to STDOUT"() { + when: + command.handle(getExecutionContext('1')) + + then: + def output = output.toString() + output.contains('DROP TABLE PUBLIC.book;') + !output.contains('DROP TABLE PUBLIC.author;') + } + + def "writes the SQL to roll back the specified number of change sets to a file given as arguments"() { + when: + command.handle(getExecutionContext('1', outputFile.canonicalPath)) + + then: + def output = outputFile.text + output.contains('DROP TABLE PUBLIC.book;') + !output.contains('DROP TABLE PUBLIC.author;') + + } + + def "an error occurs if the count parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "The ${command.name} command requires a change set number argument" + } + + def "an error occurs if the count parameter is not number"() { + when: + command.handle(getExecutionContext('one')) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'The change set number argument \'one\' isn\'t a number' + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackSqlCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackSqlCommandSpec.groovy new file mode 100644 index 00000000000..c0630467212 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackSqlCommandSpec.groovy @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException +import spock.lang.AutoCleanup + +class DbmRollbackSqlCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmRollbackSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('rollback', 'sql') + + def setup() { + command.changeLogFile << CHANGE_LOG_CONTENT + + new DbmUpdateCountCommand(applicationContext: applicationContext).handle(getExecutionContext('1')) + new DbmTagCommand(applicationContext: applicationContext).handle(getExecutionContext('test-tag')) + new DbmUpdateCommand(applicationContext: applicationContext).handle(getExecutionContext()) + + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + assert tables as Set == ['book', 'author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + def "writes SQL to roll back the database to the state it was in when the tag was applied to STDOUT"() { + when: + command.handle(getExecutionContext('test-tag')) + + then: + def output = output.toString() + output.contains('DROP TABLE PUBLIC.book;') + !output.contains('DROP TABLE PUBLIC.author;') + } + + def "writes SQL to roll back the database to the state it was in when the tag was applied to a file given as arguments"() { + when: + command.handle(getExecutionContext('test-tag', outputFile.canonicalPath)) + + then: + def output = outputFile.text + output.contains('DROP TABLE PUBLIC.book;') + !output.contains('DROP TABLE PUBLIC.author;') + + } + + def "an error occurs if tagName parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "The ${command.name} command requires a tag" + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackToDateCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackToDateCommandSpec.groovy new file mode 100644 index 00000000000..41c450a9c11 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackToDateCommandSpec.groovy @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +class DbmRollbackToDateCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmRollbackToDateCommand + } + + def setup() { + command.changeLogFile << CHANGE_LOG_CONTENT + + new DbmUpdateCommand(applicationContext: applicationContext).handle(getExecutionContext()) + sql.executeUpdate('UPDATE PUBLIC.DATABASECHANGELOG SET DATEEXECUTED = \'2015-01-02 12:00:00\' WHERE ID = \'1\'') + + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + assert tables as Set == ['book', 'author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + def "rolls back the database to the state it was in at the given date/time"() { + when: + command.handle(getExecutionContext(args as String[])) + + then: + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + tables as Set == ['author', 'databasechangeloglock', 'databasechangelog'] as Set + + where: + args << [['2015-01-03'], ['2015-01-02', '13:00:00']] + } + + def "an error occurs if the date parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'Date must be specified as two strings with the format "yyyy-MM-dd HH:mm:ss" or as one strings with the format "yyyy-MM-dd"' + } + + def "an error occurs if the date parameter is invalid format"() { + when: + command.handle(getExecutionContext(args as String[])) + + then: + def e = thrown(DatabaseMigrationException) + e.message.startsWith("Problem parsing '${args.join(' ')}' as a Date") + + where: + args << [['XXXX-01-03'], ['XXXX-01-02', '13:00:00']] + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackToDateSqlCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackToDateSqlCommandSpec.groovy new file mode 100644 index 00000000000..cca3fb770a7 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmRollbackToDateSqlCommandSpec.groovy @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException +import spock.lang.AutoCleanup + +class DbmRollbackToDateSqlCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmRollbackToDateSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('rollback', 'sql') + + def setup() { + command.changeLogFile << CHANGE_LOG_CONTENT + + new DbmUpdateCommand(applicationContext: applicationContext).handle(getExecutionContext()) + sql.executeUpdate('UPDATE PUBLIC.DATABASECHANGELOG SET DATEEXECUTED = \'2015-01-02 12:00:00\' WHERE ID = \'1\'') + + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + assert tables as Set == ['book', 'author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + def "writes SQL to roll back the database to the state it was in when the tag was applied to STDOUT"() { + when: + command.handle(getExecutionContext(args as String[])) + + then: + def output = output.toString() + output.contains('DROP TABLE PUBLIC.book;') + !output.contains('DROP TABLE PUBLIC.author;') + + where: + args << [['2015-01-03'], ['2015-01-02', '13:00:00']] + } + + def "writes SQL to roll back the database to the state it was in when the tag was applied to a file given as arguments"() { + when: + command.handle(getExecutionContext(((args << outputFile.canonicalPath) as String[]))) + + then: + def output = outputFile.text + output.contains('DROP TABLE PUBLIC.book;') + !output.contains('DROP TABLE PUBLIC.author;') + + where: + args << [['2015-01-03'], ['2015-01-02', '13:00:00']] + } + + def "an error occurs if the date parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'Date must be specified as two strings with the format "yyyy-MM-dd HH:mm:ss" or as one strings with the format "yyyy-MM-dd"' + } + + def "an error occurs if the date parameter is invalid format"() { + when: + command.handle(getExecutionContext(args as String[])) + + then: + def e = thrown(DatabaseMigrationException) + e.message.startsWith("Problem parsing '${args.join(' ')}' as a Date") + + where: + args << [['XXXX-01-03'], ['XXXX-01-02', '13:00:00']] + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmStatusCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmStatusCommandSpec.groovy new file mode 100644 index 00000000000..8c364e24f1b --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmStatusCommandSpec.groovy @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import spock.lang.AutoCleanup + +class DbmStatusCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmStatusCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('update', 'sql') + + def "outputs count or list of unrun change sets to STDOUT"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext()) + + then: + output.toString().contains('2 changesets have not been applied') + } + + def "outputs count or list of unrun change sets to a file given as arguments"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext(outputFile.canonicalPath)) + + then: + outputFile.text.contains('2 changesets have not been applied') + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + changeSet(author: "John Smith", id: "changeSet1") { + } + changeSet(author: "John Smith", id: "changeSet2") { + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCommandSpec.groovy new file mode 100644 index 00000000000..3101e35bdd8 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCommandSpec.groovy @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand + +class DbmUpdateCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmUpdateCommand + } + + def "updates database to current version"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext()) + + then: + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + tables as Set == ['book', 'author', 'databasechangeloglock', 'databasechangelog'] as Set + + and: + def authors = sql.rows('SELECT name FROM author').collect { it.name } + authors as Set == ['Mary', 'Amelia'] as Set + } + + def "updates database to current version with contexts"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext('--contexts=test')) + + then: + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + tables as Set == ['book', 'author', 'databasechangeloglock', 'databasechangelog'] as Set + + and: + def authors = sql.rows('SELECT name FROM author').collect { it.name } + authors as Set == ['Amelia'] as Set + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "3") { + addForeignKeyConstraint(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK_4sac2ubmnqva85r8bk8fxdvbf", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author") + } + + changeSet(author: "John Smith", id: "4", context: "development") { + insert(tableName: "author") { + column(name: "name", value: "Mary") + column(name: "version", value: "0") + } + } + + changeSet(author: "John Smith", id: "5", context: "test") { + insert(tableName: "author") { + column(name: "name", value: "Amelia") + column(name: "version", value: "0") + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCountCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCountCommandSpec.groovy new file mode 100644 index 00000000000..aa4a776c5ca --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCountCommandSpec.groovy @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException + +class DbmUpdateCountCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmUpdateCountCommand + } + + def "applies next NUM changes to the database"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext('1')) + + then: + def tables = sql.rows('SELECT table_name FROM information_schema.tables WHERE table_class = \'org.h2.mvstore.db.MVTable\'').collect { it.table_name.toLowerCase() } + tables as Set == ['author', 'databasechangeloglock', 'databasechangelog'] as Set + } + + def "an error occurs if the count parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "The ${command.name} command requires a change set number argument" + } + + def "an error occurs if the count parameter is not number"() { + when: + command.handle(getExecutionContext('one')) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'The change set number argument \'one\' isn\'t a number' + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCountSqlCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCountSqlCommandSpec.groovy new file mode 100644 index 00000000000..676ec82a072 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateCountSqlCommandSpec.groovy @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import org.grails.plugins.databasemigration.DatabaseMigrationException +import spock.lang.AutoCleanup + +class DbmUpdateCountSqlCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmUpdateCountSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('update', 'sql') + + def "writes SQL to apply next NUM changes to the database to STDOUT"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext('1')) + + then: + def output = output.toString() + output.contains('CREATE TABLE PUBLIC.author (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT authorPK PRIMARY KEY (id));') + !output.contains('CREATE TABLE PUBLIC.book (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, author_id BIGINT NOT NULL, title VARCHAR(255) NOT NULL, CONSTRAINT bookPK PRIMARY KEY (id));') + } + + def "writes SQL to apply next NUM changes to the database to a file given as arguments"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext('1', outputFile.canonicalPath)) + + then: + def output = outputFile.text + output.contains('CREATE TABLE PUBLIC.author (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT authorPK PRIMARY KEY (id));') + !output.contains('CREATE TABLE PUBLIC.book (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, author_id BIGINT NOT NULL, title VARCHAR(255) NOT NULL, CONSTRAINT bookPK PRIMARY KEY (id));') + } + + def "an error occurs if the count parameter is not specified"() { + when: + command.handle(getExecutionContext()) + + then: + def e = thrown(DatabaseMigrationException) + e.message == "The ${command.name} command requires a change set number argument" + } + + def "an error occurs if the count parameter is not number"() { + when: + command.handle(getExecutionContext('one')) + + then: + def e = thrown(DatabaseMigrationException) + e.message == 'The change set number argument \'one\' isn\'t a number' + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "3") { + addForeignKeyConstraint(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK_4sac2ubmnqva85r8bk8fxdvbf", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author") + } + + changeSet(author: "John Smith", id: "4", context: "development") { + insert(tableName: "author") { + column(name: "name", value: "Mary") + column(name: "version", value: "0") + } + } + + changeSet(author: "John Smith", id: "5", context: "test") { + insert(tableName: "author") { + column(name: "name", value: "Amelia") + column(name: "version", value: "0") + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateSqlCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateSqlCommandSpec.groovy new file mode 100644 index 00000000000..d04a5426318 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmUpdateSqlCommandSpec.groovy @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import spock.lang.AutoCleanup + +class DbmUpdateSqlCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmUpdateSqlCommand + } + + @AutoCleanup('delete') + File outputFile = File.createTempFile('update', 'sql') + + def "writes SQL to update database to STDOUT"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext()) + + then: + def output = output.toString() + output.contains('CREATE TABLE PUBLIC.author (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT authorPK PRIMARY KEY (id));') + output.contains('CREATE TABLE PUBLIC.book (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, author_id BIGINT NOT NULL, title VARCHAR(255) NOT NULL, CONSTRAINT bookPK PRIMARY KEY (id));') + output.contains('ALTER TABLE PUBLIC.book ADD CONSTRAINT FK_4sac2ubmnqva85r8bk8fxdvbf FOREIGN KEY (author_id) REFERENCES PUBLIC.author (id);') + output.contains('INSERT INTO PUBLIC.author (name, version) VALUES (\'Mary\', \'0\');') + output.contains('INSERT INTO PUBLIC.author (name, version) VALUES (\'Amelia\', \'0\');') + } + + def "writes SQL to update database with contexts"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext('--contexts=test')) + + then: + def output = output.toString() + output.contains('CREATE TABLE PUBLIC.author (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT authorPK PRIMARY KEY (id));') + output.contains('CREATE TABLE PUBLIC.book (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, author_id BIGINT NOT NULL, title VARCHAR(255) NOT NULL, CONSTRAINT bookPK PRIMARY KEY (id));') + output.contains('ALTER TABLE PUBLIC.book ADD CONSTRAINT FK_4sac2ubmnqva85r8bk8fxdvbf FOREIGN KEY (author_id) REFERENCES PUBLIC.author (id);') + !output.contains('INSERT INTO PUBLIC.author (name, version) VALUES (\'Mary\', \'0\');') + output.contains('INSERT INTO PUBLIC.author (name, version) VALUES (\'Amelia\', \'0\');') + } + + def "writes SQL to update database to a file given as arguments"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + when: + command.handle(getExecutionContext(outputFile.canonicalPath)) + + then: + def output = outputFile.text + output.contains('CREATE TABLE PUBLIC.author (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT authorPK PRIMARY KEY (id));') + output.contains('CREATE TABLE PUBLIC.book (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, version BIGINT NOT NULL, author_id BIGINT NOT NULL, title VARCHAR(255) NOT NULL, CONSTRAINT bookPK PRIMARY KEY (id));') + output.contains('ALTER TABLE PUBLIC.book ADD CONSTRAINT FK_4sac2ubmnqva85r8bk8fxdvbf FOREIGN KEY (author_id) REFERENCES PUBLIC.author (id);') + output.contains('INSERT INTO PUBLIC.author (name, version) VALUES (\'Mary\', \'0\');') + output.contains('INSERT INTO PUBLIC.author (name, version) VALUES (\'Amelia\', \'0\');') + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "3") { + addForeignKeyConstraint(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK_4sac2ubmnqva85r8bk8fxdvbf", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author") + } + + changeSet(author: "John Smith", id: "4", context: "development") { + insert(tableName: "author") { + column(name: "name", value: "Mary") + column(name: "version", value: "0") + } + } + + changeSet(author: "John Smith", id: "5", context: "test") { + insert(tableName: "author") { + column(name: "name", value: "Amelia") + column(name: "version", value: "0") + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmValidateCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmValidateCommandSpec.groovy new file mode 100644 index 00000000000..b49d0db764b --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/DbmValidateCommandSpec.groovy @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.dev.commands.ApplicationCommand +import liquibase.exception.CommandExecutionException + +class DbmValidateCommandSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + @Override + protected Class getCommandClass() { + return DbmValidateCommand + } + + def "checks the valid changelog"() { + given: + command.changeLogFile << CHANGE_LOG_CONTENT + + expect: + command.handle(getExecutionContext()) + } + + def "checks the invalid changelog"() { + given: + command.changeLogFile << 'xxx' + + when: + command.handle(getExecutionContext()) + + then: + thrown(CommandExecutionException) + } + + static final String CHANGE_LOG_CONTENT = ''' +databaseChangeLog = { + + changeSet(author: "John Smith", id: "1") { + createTable(tableName: "author") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "authorPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "name", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } + + changeSet(author: "John Smith", id: "2") { + createTable(tableName: "book") { + column(autoIncrement: "true", name: "id", type: "BIGINT") { + constraints(primaryKey: "true", primaryKeyName: "bookPK") + } + + column(name: "version", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "author_id", type: "BIGINT") { + constraints(nullable: "false") + } + + column(name: "title", type: "VARCHAR(255)") { + constraints(nullable: "false") + } + } + } +} +''' +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ScriptDatabaseMigrationCommandSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ScriptDatabaseMigrationCommandSpec.groovy new file mode 100644 index 00000000000..05d5f36cc28 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/command/ScriptDatabaseMigrationCommandSpec.groovy @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.command + +import grails.util.GrailsNameUtils +import org.grails.build.parsing.CommandLineParser +import org.grails.cli.GrailsCli +import org.grails.cli.profile.ExecutionContext +import org.grails.config.CodeGenConfig +import org.h2.Driver + +abstract class ScriptDatabaseMigrationCommandSpec extends DatabaseMigrationCommandSpec { + + ScriptDatabaseMigrationCommand command + + CodeGenConfig config + + def setup() { + def configMap = [ + 'grails.plugin.databasemigration.changelogLocation': changeLogLocation.canonicalPath, + 'dataSource.url' : 'jdbc:h2:mem:testDb', + 'dataSource.username' : 'sa', + 'dataSource.password' : '', + 'dataSource.driverClassName' : Driver.name, + 'environments.other.dataSource.url' : 'jdbc:h2:mem:otherDb', + ] + config = new CodeGenConfig() + config.mergeMap(configMap) + config.mergeMap(configMap, true) + + command = commandClass.newInstance() + command.config = config + command.changeLogFile.parentFile.mkdirs() + } + + abstract protected Class getCommandClass() + + protected ExecutionContext getExecutionContext(String... args) { + def executionContext = new GrailsCli.ExecutionContextImpl(config) + executionContext.commandLine = new CommandLineParser().parse(([GrailsNameUtils.getScriptName(GrailsNameUtils.getLogicalName(commandClass.name, 'Command'))] + args.toList()) as String[]) + executionContext + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogSpec.groovy new file mode 100644 index 00000000000..0059c43fa27 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GroovyChangeLogSpec.groovy @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import grails.core.GrailsApplication +import liquibase.exception.CommandExecutionException +import org.grails.plugins.databasemigration.command.ApplicationContextDatabaseMigrationCommandSpec +import org.grails.plugins.databasemigration.command.DbmChangelogSyncCommand +import org.grails.plugins.databasemigration.command.DbmRollbackCommand +import org.grails.plugins.databasemigration.command.DbmTagCommand +import org.grails.plugins.databasemigration.command.DbmUpdateCommand +import org.grails.plugins.databasemigration.command.DbmUpdateCountCommand + +class GroovyChangeLogSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + static List calledBlocks + + def setup() { + calledBlocks = [] + Locale.setDefault(new Locale("en", "US")) + } + + def "updates a database with Groovy Change"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: "John Smith", id: "1") { + grailsChange { + init { ${GroovyChangeLogSpec.name}.calledBlocks << 'init' } + validate { ${GroovyChangeLogSpec.name}.calledBlocks << 'validate' } + change { ${GroovyChangeLogSpec.name}.calledBlocks << 'change' } + rollback { ${GroovyChangeLogSpec.name}.calledBlocks << 'rollback' } + confirm 'confirmation message' + checkSum 'override value for checksum' + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + calledBlocks == ['init', 'validate', 'change'] + output.toString().contains('confirmation message') + } + + + def "outputs a warning message by calling the warn method"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: "John Smith", id: "2") { + grailsChange { + validate { + ${GroovyChangeLogSpec.name}.calledBlocks << 'validate' + warn('warn message') + } + change { + ${GroovyChangeLogSpec.name}.calledBlocks << 'change' + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + output.toString().contains('warn message') + calledBlocks == ['validate', 'change'] + } + + def "stops processing by calling the error method"() { + given: + DbmUpdateCommand command = (DbmUpdateCommand) createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: "John Smith", id: "1") { + grailsChange { + validate { + ${GroovyChangeLogSpec.name}.calledBlocks << 'validate' + error('error message') + } + change { + ${GroovyChangeLogSpec.name}.calledBlocks << 'change' + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + def e = thrown(CommandExecutionException) + + e.message.contains('1 changes have validation failures') + e.message.contains('error message, changelog.groovy::1::John Smith') + calledBlocks == ['validate'] + } + + + def "can use bind variables in the change block"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: "John Smith", id: "4") { + grailsChange { + change { + assert changeSet.id == '4' + assert resourceAccessor.toString().startsWith('CompositeResourceAccessor{') + assert ctx.hashCode() == ${applicationContext.hashCode()} + assert application.hashCode() == ${applicationContext.getBean(GrailsApplication).hashCode()} + ${GroovyChangeLogSpec.name}.calledBlocks << 'change' + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + calledBlocks == ['change'] + } + + + def "executes sql statements in the change block"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +import groovy.sql.Sql +import liquibase.statement.core.InsertStatement + +databaseChangeLog = { + changeSet(author: "John Smith", id: "5") { + grailsChange { + change { + new Sql(database.connection.underlyingConnection).executeUpdate('CREATE TABLE book (id INT)') + new Sql(databaseConnection.underlyingConnection).executeUpdate('INSERT INTO book (id) VALUES (1)') + new Sql(connection).executeUpdate('INSERT INTO book (id) VALUES (2)') + sqlStatement(new InsertStatement(null, null, 'book').addColumnValue('id', 3)) + sqlStatements([new InsertStatement(null, null, 'book').addColumnValue('id', 4), new InsertStatement(null, null, 'book').addColumnValue('id', 5)]) + } + } + } +} +""" + + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + sql.rows('SELECT id FROM book').collect { it.id } as Set == [1, 2, 3, 4, 5] as Set + } + + + def "rolls back a database with Groovy Change"() { + given: + def command = createCommand(DbmRollbackCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: "John Smith", id: "6") { + } + changeSet(author: "John Smith", id: "7") { + grailsChange { + init { ${GroovyChangeLogSpec.name}.calledBlocks << 'init' } + validate { ${GroovyChangeLogSpec.name}.calledBlocks << 'validate' } + change { ${GroovyChangeLogSpec.name}.calledBlocks << 'change' } + rollback { ${GroovyChangeLogSpec.name}.calledBlocks << 'rollback' } + confirm 'confirmation message' + checkSum 'override value for checksum' + } + } +} +""" + createCommand(DbmUpdateCountCommand).handle(getExecutionContext(DbmUpdateCountCommand, '1')) + createCommand(DbmTagCommand).handle(getExecutionContext(DbmTagCommand, 'test tag')) + createCommand(DbmChangelogSyncCommand).handle(getExecutionContext(DbmChangelogSyncCommand)) + calledBlocks = [] + + when: + command.handle(getExecutionContext(DbmRollbackCommand, 'test tag')) + + then: + calledBlocks == ['init', 'change', 'rollback', 'rollback'] + } + + + def "can use bind variables in the rollback block"() { + given: + def command = createCommand(DbmRollbackCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: "John Smith", id: "8") { + } + changeSet(author: "John Smith", id: "9") { + grailsChange { + rollback { + assert changeSet.id == '9' + assert resourceAccessor.toString().startsWith('CompositeResourceAccessor{') + assert ctx.hashCode() == ${applicationContext.hashCode()} + assert application.hashCode() == ${applicationContext.getBean(GrailsApplication).hashCode()} + ${GroovyChangeLogSpec.name}.calledBlocks << 'rollback' + } + } + } +} +""" + createCommand(DbmUpdateCountCommand).handle(getExecutionContext(DbmUpdateCountCommand, '1')) + createCommand(DbmTagCommand).handle(getExecutionContext(DbmTagCommand, 'test tag')) + createCommand(DbmChangelogSyncCommand).handle(getExecutionContext(DbmChangelogSyncCommand)) + calledBlocks = [] + + when: + command.handle(getExecutionContext(DbmRollbackCommand, 'test tag')) + + then: + calledBlocks == ['rollback', 'rollback'] + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GroovyPreconditionSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GroovyPreconditionSpec.groovy new file mode 100644 index 00000000000..6118abadba1 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GroovyPreconditionSpec.groovy @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.liquibase + +import liquibase.exception.CommandExecutionException +import org.grails.plugins.databasemigration.command.ApplicationContextDatabaseMigrationCommandSpec +import org.grails.plugins.databasemigration.command.DbmUpdateCommand + +class GroovyPreconditionSpec extends ApplicationContextDatabaseMigrationCommandSpec { + + + static List executedChangeSets + + def setup() { + executedChangeSets = [] + } + def cleanup() { + executedChangeSets.clear() + } + + def "changeSet precondition is satisfied"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: 'John Smith', id: '1') { + preConditions { + grailsPrecondition { + check { + assert true + } + } + } + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + executedChangeSets == ['1'] + } + + def "changeSet precondition is not satisfied by using a simple assertion"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: 'John Smith', id: '1') { + preConditions(onFail: 'CONTINUE') { + grailsPrecondition { + check { + assert false + } + } + } + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } + changeSet(author: 'John Smith', id: '2') { + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + executedChangeSets == ['2'] + } + + def "changeSet precondition is not satisfied by using an assertion with a message"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: 'John Smith', id: '1') { + preConditions(onFail: 'CONTINUE') { + grailsPrecondition { + check { + assert false: 'precondition is not satisfied' + } + } + } + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } + changeSet(author: 'John Smith', id: '2') { + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + executedChangeSets == ['2'] + } + + def "changeSet precondition is not satisfied by calling the fail method"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: 'John Smith', id: '1') { + preConditions(onFail: 'CONTINUE') { + grailsPrecondition { + check { + fail('precondition is not satisfied') + } + } + } + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } + changeSet(author: 'John Smith', id: '2') { + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + executedChangeSets == ['2'] + } + + def "changeSet precondition is not satisfied by throwing an exception"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: 'John Smith', id: '1') { + preConditions(onError: 'CONTINUE') { + grailsPrecondition { + check { + throw new RuntimeException('precondition is not satisfied') + } + } + } + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } + changeSet(author: 'John Smith', id: '2') { + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + executedChangeSets == ['2'] + } + + def "databaseChangeLog precondition is not satisfied"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + preConditions { + grailsPrecondition { + check { + assert false + } + } + } + changeSet(author: 'John Smith', id: '1') { + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } + changeSet(author: 'John Smith', id: '2') { + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + def e = thrown(CommandExecutionException) + e.message.contains('1 preconditions failed') + executedChangeSets == [] + } + + def "checks the available variables"() { + given: + def command = createCommand(DbmUpdateCommand) + command.changeLogFile << """ +databaseChangeLog = { + changeSet(author: 'John Smith', id: '1') { + preConditions(onError: 'CONTINUE') { + grailsPrecondition { + check { + assert database + assert databaseConnection + assert connection + assert sql + assert resourceAccessor + assert ctx + assert application + assert changeSet + assert changeLog + } + } + } + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } + changeSet(author: 'John Smith', id: '2') { + grailsChange { + change { + ${GroovyPreconditionSpec.name}.executedChangeSets << changeSet.id + } + } + } +} +""" + when: + command.handle(getExecutionContext(DbmUpdateCommand)) + + then: + executedChangeSets == ['1','2'] + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/testing/OutputCaptureExtension.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/testing/OutputCaptureExtension.groovy new file mode 100644 index 00000000000..8cb4988c81f --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/testing/OutputCaptureExtension.groovy @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.testing + +import groovy.transform.TupleConstructor +import org.grails.plugins.databasemigration.testing.annotation.OutputCapture +import org.spockframework.runtime.IStandardStreamsListener +import org.spockframework.runtime.InvalidSpecException +import org.spockframework.runtime.StandardStreamsCapturer +import org.spockframework.runtime.extension.IAnnotationDrivenExtension +import org.spockframework.runtime.extension.IMethodInvocation +import org.spockframework.runtime.model.FieldInfo +import org.spockframework.runtime.model.SpecInfo + +import java.nio.charset.StandardCharsets + +class OutputCaptureExtension implements IAnnotationDrivenExtension { + + private final Map fieldBuffers = new HashMap(1); + + @Override + void visitFieldAnnotation(OutputCapture annotation, FieldInfo field) { + if (!field.type.isAssignableFrom(Object.class)) { + throw new InvalidSpecException("""Wrong type for field %s. + |@OutputCapture can only be placed on fields assignableFrom Object. + |For example + |@OutputCapture Object output + |""".stripMargin()).withArgs(field.name) + } + this.fieldBuffers[field] = new ByteArrayOutputStream() + } + + + @Override + void visitSpec(SpecInfo spec) { + def capturer = new StandardStreamsCapturer() + capturer.addStandardStreamsListener(new Listener(fieldBuffers)) + capturer.start() + spec.addSharedInitializerInterceptor({ IMethodInvocation invocation -> + fieldBuffers.keySet().each { field -> + if (field.shared) { + fieldBuffers[field] = new ByteArrayOutputStream() + invocation.instance.metaClass.setProperty(invocation.instance, field.reflection.name, createNewOutput(fieldBuffers[field])) + } + } + invocation.proceed() + }) + spec.addInitializerInterceptor({ IMethodInvocation invocation -> + fieldBuffers.keySet().each { field -> + if (!field.shared) { + fieldBuffers[field] = new ByteArrayOutputStream() + invocation.instance.metaClass.setProperty(invocation.instance, field.reflection.name, createNewOutput(fieldBuffers[field])) + } + } + invocation.proceed() + }) + spec.addCleanupSpecInterceptor({ IMethodInvocation invocation -> + capturer.stop() + invocation.proceed() + }) + } + + private Object createNewOutput(ByteArrayOutputStream baos) { + new Object() { + + boolean contains(CharSequence s) { + this.toString().contains(s) + } + + @Override + String toString() { + return baos.toString(StandardCharsets.UTF_8) + } + } + } + + @TupleConstructor(includeFields = true) + static class Listener implements IStandardStreamsListener { + + private Map fieldBuffers + + @Override + void standardOut(String message) { + fieldBuffers.values().each { baos -> + new PrintStream(baos).append(message) + } + } + + @Override + void standardErr(String message) { + fieldBuffers.values().each { baos -> + new PrintStream(baos).append(message) + } + } + + } +} diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/testing/annotation/OutputCapture.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/testing/annotation/OutputCapture.groovy new file mode 100644 index 00000000000..43c4fa96a93 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/testing/annotation/OutputCapture.groovy @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.databasemigration.testing.annotation + +import org.grails.plugins.databasemigration.testing.OutputCaptureExtension +import org.spockframework.runtime.extension.ExtensionAnnotation + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@ExtensionAnnotation(OutputCaptureExtension) +@interface OutputCapture { + +} \ No newline at end of file diff --git a/grails-data-hibernate7/dbmigration/src/test/resources/logback.groovy b/grails-data-hibernate7/dbmigration/src/test/resources/logback.groovy new file mode 100644 index 00000000000..f4b25c65a23 --- /dev/null +++ b/grails-data-hibernate7/dbmigration/src/test/resources/logback.groovy @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +// See http://logback.qos.ch/manual/groovy.html for details on configuration +def CONSOLE_LOG_PATTERN = '%d{HH:mm:ss.SSS} [%t] %highlight(%p) %cyan(\\(%logger{39}\\)) %m%n' + +appender('STDOUT', ConsoleAppender) { + withJansi = true + encoder(PatternLayoutEncoder) { + pattern = CONSOLE_LOG_PATTERN + } +} +root(ERROR, ['STDOUT']) + +//logger("org.grails", DEBUG, ['STDOUT'], false) +logger("liquibase", DEBUG, ['STDOUT'], false) +//logger("groovy.sql", DEBUG, ['STDOUT'], false) +//logger("org.hibernate.SQL", DEBUG, ["STDOUT"], false) +logger("org.grails.datastore.gorm.GormEnhancer", INFO, ['STDOUT'], false) +logger("org.grails.plugin.datasource.TomcatJDBCPoolMBeanExporter", WARN, ['STDOUT'], false) + + + diff --git a/grails-data-hibernate7/docs/build.gradle b/grails-data-hibernate7/docs/build.gradle new file mode 100644 index 00000000000..c8cc940f3ef --- /dev/null +++ b/grails-data-hibernate7/docs/build.gradle @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.asciidoctor.gradle.jvm.AsciidoctorTask + +plugins { + id 'groovy' + id 'org.asciidoctor.jvm.convert' + id 'org.apache.grails.buildsrc.dependency-validator' +} + +version = projectVersion + +ext { + isReleaseVersion = !projectVersion.endsWith('-SNAPSHOT') + coreProjects = ['grails-datastore-core', 'grails-datamapping-core'] +} + +apply plugin: 'org.apache.grails.buildsrc.groovydoc' + +dependencies { + documentation platform(project(':grails-bom')) + documentation 'com.github.javaparser:javaparser-core' + documentation "info.picocli:picocli:$picocliVersion" + documentation 'org.apache.groovy:groovy-dateutil' + documentation 'org.apache.groovy:groovy-ant' + documentation 'org.apache.groovy:groovy-groovydoc' + documentation 'org.apache.groovy:groovy-templates' + documentation 'org.fusesource.jansi:jansi' + documentation 'jline:jline' + documentation project(':grails-bootstrap') + documentation project(':grails-core') + documentation project(':grails-spring') + documentation 'org.hibernate:hibernate-core-jakarta' + coreProjects.each { + documentation "org.apache.grails.data:$it" + } + rootProject.subprojects + .findAll { it.findProperty('gormApiDocs') } + .each { documentation project(":$it.name") } +} + +tasks.named('asciidoctor', AsciidoctorTask) { AsciidoctorTask it -> + it.inputs.dir(layout.projectDirectory.dir('src/docs/asciidoc')).withPropertyName('docsSrcDir').withPathSensitivity(PathSensitivity.RELATIVE) + it.outputs.dir layout.buildDirectory.dir('asciidoc/manual') + + it.jvm { + jvmArgs('--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED', '--add-opens', 'java.base/java.io=ALL-UNNAMED') + } + it.baseDirFollowsSourceFile() + it.sourceDir layout.projectDirectory.dir('src/docs/asciidoc') + it.outputDir = layout.buildDirectory.dir('asciidoc/manual') + + resources { + from(project.layout.projectDirectory.dir("src/docs/asciidoc/images")) + into './images' + } + + it.attributes = [ + 'experimental': 'true', + 'compat-mode': 'true', + 'toc': 'left', + 'icons': 'font', + 'reproducible': '', + 'version': projectVersion, + 'pluginVersion': projectVersion, + 'groupId': project.group, + 'artifactId': project.name, + 'migrationPluginExamplesDir': project.layout.projectDirectory.dir('src/docs/asciidoc/databaseMigration').asFile.relativePath(rootProject.findProject(':grails-data-hibernate7-dbmigration').layout.projectDirectory.asFile), + 'migrationPluginGroupId': rootProject.findProject(':grails-data-hibernate7-dbmigration').group, + 'migrationPluginArtifactId': rootProject.findProject(':grails-data-hibernate7-dbmigration').name, + 'liquibaseHibernate5Version': liquibaseHibernate5Version + ] +} + +tasks.withType(Groovydoc).configureEach { + it.dependsOn(rootProject.subprojects + .findAll { it.findProperty('gormApiDocs') } + .collect { ":${it.name}:groovydoc" }) + + it.docTitle = "GORM for Hibernate 7 - $project.version" + + def sourceFiles = coreProjects.collect { + rootProject.layout.projectDirectory.files("$it/src/main/groovy") + }.sum() + + rootProject.subprojects + .findAll { it.findProperty('gormApiDocs') } + .each { sourceFiles += it.files('src/main/groovy') } + + it.source = sourceFiles + it.destinationDir = layout.buildDirectory.dir('combined-api/api').get().asFile + it.classpath = configurations.documentation + + List groovydocSrcDirs = coreProjects.collect { + rootProject.layout.projectDirectory.dir("$it/src/main/groovy").asFile + } + rootProject.subprojects + .findAll { sp -> sp.findProperty('gormApiDocs') } + .each { sp -> groovydocSrcDirs << new File(sp.projectDir, 'src/main/groovy') } + it.ext.groovydocSourceDirs = groovydocSrcDirs +} + +tasks.register('docs', Sync).configure { Sync docTask -> + docTask.group = 'documentation' + docTask.dependsOn('asciidoctor', 'groovydoc') + + def apiDir = layout.buildDirectory.dir('combined-api') + def resourceDir = layout.projectDirectory.dir('src/docs/resources') + def guideDir = layout.buildDirectory.dir('asciidoc') + + docTask.from apiDir, resourceDir, guideDir + docTask.into layout.buildDirectory.dir('docs') + + docTask.finalizedBy('assembleDocsDist') +} + +tasks.register('assembleDocsDist', Zip).configure { Zip it -> + it.group = 'documentation' + it.dependsOn('docs') + it.from(layout.buildDirectory.dir('docs')) + it.archiveFileName = "${project.name}-${project.version}.zip" + it.destinationDirectory = project.layout.buildDirectory.dir('distributions') +} + +// the groovy plugin is applied to this project, but we do not build a jar file since +// the dependencies are only used for resolving versions from the bom +tasks.withType(Jar).configureEach { + enabled = false +} diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures.adoc new file mode 100644 index 00000000000..e8055cf00c2 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures.adoc @@ -0,0 +1,20 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +The following sections cover more advanced usages of GORM including caching, custom mapping and events. \ No newline at end of file diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/defaultSortOrder.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/defaultSortOrder.adoc new file mode 100644 index 00000000000..6c331e2ce0b --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/defaultSortOrder.adoc @@ -0,0 +1,67 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +You can sort objects using query arguments such as those found in the <> method: + +[source,java] +---- +def airports = Airport.list(sort:'name') +---- + +However, you can also declare the default sort order for a collection in the mapping: + +[source,java] +---- +class Airport { + ... + static mapping = { + sort "name" + } +} +---- + +The above means that all collections of `Airport` instances will by default be sorted by the airport name. If you also want to change the sort _order_, use this syntax: + +[source,java] +---- +class Airport { + ... + static mapping = { + sort name: "desc" + } +} +---- + +Finally, you can configure sorting at the association level: + +[source,java] +---- +class Airport { + ... + static hasMany = [flights: Flight] + + static mapping = { + flights sort: 'number', order: 'desc' + } +} +---- + +In this case, the `flights` collection will always be sorted in descending order of flight number. + +WARNING: These mappings will not work for default unidirectional one-to-many or many-to-many relationships because they involve a join table. See <> for more details. Consider using a `SortedSet` or queries with sort parameters to fetch the data you need. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/eventsAutoTimestamping.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/eventsAutoTimestamping.adoc new file mode 100644 index 00000000000..d71e8b52519 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/eventsAutoTimestamping.adoc @@ -0,0 +1,410 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +GORM supports the registration of events as methods that get fired when certain events occurs such as deletes, inserts and updates. The following is a list of supported events: + +* `beforeInsert` - Executed before an object is initially persisted to the database. If you return false, the insert will be cancelled. +* `beforeUpdate` - Executed before an object is updated. If you return false, the update will be cancelled. +* `beforeDelete` - Executed before an object is deleted. If you return false, the operation delete will be cancelled. +* `beforeValidate` - Executed before an object is validated +* `afterInsert` - Executed after an object is persisted to the database +* `afterUpdate` - Executed after an object has been updated +* `afterDelete` - Executed after an object has been deleted +* `onLoad` - Executed when an object is loaded from the database + +To add an event simply register the relevant method with your domain class. + +WARNING: Do not attempt to flush the session within an event (such as with obj.save(flush:true)). Since events are fired during flushing this will cause a StackOverflowError. + + +==== The beforeInsert event + + +Fired before an object is saved to the database + +[source,java] +---- +class Person { + private static final Date NULL_DATE = new Date(0) + + String firstName + String lastName + Date signupDate = NULL_DATE + + def beforeInsert() { + if (signupDate == NULL_DATE) { + signupDate = new Date() + } + } +} +---- + + +==== The beforeUpdate event + + +Fired before an existing object is updated + +[source,java] +---- +class Person { + + def securityService + + String firstName + String lastName + String lastUpdatedBy + + static constraints = { + lastUpdatedBy nullable: true + } + + static mapping = { + autowire true + } + + def beforeUpdate() { + lastUpdatedBy = securityService.currentAuthenticatedUsername() + } +} +---- + +Notice the usage of `autowire true` above. This is required for the bean `securityService` to be injected. + + +==== The beforeDelete event + + +Fired before an object is deleted. + +[source,java] +---- +class Person { + String name + + def beforeDelete() { + ActivityTrace.withNewSession { + new ActivityTrace(eventName: "Person Deleted", data: name).save() + } + } +} +---- + +Notice the usage of `withNewSession` method above. Since events are triggered whilst Hibernate is flushing using persistence methods like `save()` and `delete()` won't result in objects being saved unless you run your operations with a new `Session`. + +Fortunately the `withNewSession` method lets you share the same transactional JDBC connection even though you're using a different underlying `Session`. + + +==== The beforeValidate event + + +Fired before an object is validated. + +[source,java] +---- +class Person { + String name + + static constraints = { + name size: 5..45 + } + + def beforeValidate() { + name = name?.trim() + } +} +---- + +The `beforeValidate` method is run before any validators are run. + +NOTE: Validation may run more often than you think. It is triggered by the `validate()` and `save()` methods as you'd expect, but it is also typically triggered just before the view is rendered as well. So when writing `beforeValidate()` implementations, make sure that they can handle being called multiple times with the same property values. + +GORM supports an overloaded version of `beforeValidate` which accepts a `List` parameter which may include +the names of the properties which are about to be validated. This version of `beforeValidate` will be called +when the `validate` method has been invoked and passed a `List` of property names as an argument. + +[source,java] +---- +class Person { + String name + String town + Integer age + + static constraints = { + name size: 5..45 + age range: 4..99 + } + + def beforeValidate(List propertiesBeingValidated) { + // do pre validation work based on propertiesBeingValidated + } +} + +def p = new Person(name: 'Jacob Brown', age: 10) +p.validate(['age', 'name']) +---- + +NOTE: Note that when `validate` is triggered indirectly because of a call to the `save` method that +the `validate` method is being invoked with no arguments, not a `List` that includes all of +the property names. + +Either or both versions of `beforeValidate` may be defined in a domain class. GORM will +prefer the `List` version if a `List` is passed to `validate` but will fall back on the +no-arg version if the `List` version does not exist. Likewise, GORM will prefer the +no-arg version if no arguments are passed to `validate` but will fall back on the +`List` version if the no-arg version does not exist. In that case, `null` is passed to `beforeValidate`. + + +==== The onLoad/beforeLoad event + + +Fired immediately before an object is loaded from the database: + +[source,java] +---- +class Person { + String name + Date dateCreated + Date lastUpdated + + def onLoad() { + log.debug "Loading ${id}" + } +} +---- + +`beforeLoad()` is effectively a synonym for `onLoad()`, so only declare one or the other. + + +==== The afterLoad event + + +Fired immediately after an object is loaded from the database: + +[source,java] +---- +class Person { + String name + Date dateCreated + Date lastUpdated + + def afterLoad() { + name = "I'm loaded" + } +} +---- + + +==== Custom Event Listeners + +To register a custom event listener you need to subclass `AbstractPersistenceEventListener` (in package _org.grails.datastore.mapping.engine.event_) and implement the methods `onPersistenceEvent` and `supportsEventType`. You also must provide a reference to the datastore to the listener. The simplest possible implementation can be seen below: + +[source,groovy] +---- +public MyPersistenceListener(final Datastore datastore) { + super(datastore) +} + +@Override +protected void onPersistenceEvent(final AbstractPersistenceEvent event) { + switch(event.eventType) { + case PreInsert: + println "PRE INSERT \${event.entityObject}" + break + case PostInsert: + println "POST INSERT \${event.entityObject}" + break + case PreUpdate: + println "PRE UPDATE \${event.entityObject}" + break; + case PostUpdate: + println "POST UPDATE \${event.entityObject}" + break; + case PreDelete: + println "PRE DELETE \${event.entityObject}" + break; + case PostDelete: + println "POST DELETE \${event.entityObject}" + break; + case PreLoad: + println "PRE LOAD \${event.entityObject}" + break; + case PostLoad: + println "POST LOAD \${event.entityObject}" + break; + } +} + +@Override +public boolean supportsEventType(Class eventType) { + return true +} +---- + +The `AbstractPersistenceEvent` class has many subclasses (`PreInsertEvent`, `PostInsertEvent` etc.) that provide further information specific to the event. A `cancel()` method is also provided on the event which allows you to veto an insert, update or delete operation. + +Once you have created your event listener you need to register it. If you are using Spring this can be done via the `ApplicationContext`: + +[source,groovy] +---- +HibernateDatastore datastore = applicationContext.getBean(HibernateDatastore) +applicationContext.addApplicationListener new MyPersistenceListener(datastore) +---- + +If you are not using Spring then you can register the event listener using the `getApplicationEventPublisher()` method: + +[source,groovy] +---- +HibernateDatastore datastore = ... // get a reference to the datastore +datastore.getApplicationEventPublisher() + .addApplicationListener new MyPersistenceListener(datastore) +---- + + +==== Hibernate Events + + +It is generally encouraged to use the non-Hibernate specific API described above, but if you need access to more detailed Hibernate events then you can define custom Hibernate-specific event listeners. + +You can also register event handler classes in an application's `grails-app/conf/spring/resources.groovy` or in the `doWithSpring` closure in a plugin descriptor by registering a Spring bean named `hibernateEventListeners`. This bean has one property, `listenerMap` which specifies the listeners to register for various Hibernate events. + +The values of the Map are instances of classes that implement one or more Hibernate listener interfaces. You can use one class that implements all of the required interfaces, or one concrete class per interface, or any combination. The valid Map keys and corresponding interfaces are listed here: + +[format="csv", options="header"] +|=== + +*Name*,*Interface* +auto-flush,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/AutoFlushEventListener.html[AutoFlushEventListener] +merge,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/MergeEventListener.html[MergeEventListener] +create,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PersistEventListener.html[PersistEventListener] +create-onflush,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PersistEventListener.html[PersistEventListener] +delete,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/DeleteEventListener.html[DeleteEventListener] +dirty-check,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/DirtyCheckEventListener.html[DirtyCheckEventListener] +evict,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/EvictEventListener.html[EvictEventListener] +flush,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/FlushEventListener.html[FlushEventListener] +flush-entity,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/FlushEntityEventListener.html[FlushEntityEventListener] +load,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/LoadEventListener.html[LoadEventListener] +load-collection,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/InitializeCollectionEventListener.html[InitializeCollectionEventListener] +lock,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/LockEventListener.html[LockEventListener] +refresh,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/RefreshEventListener.html[RefreshEventListener] +replicate,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/ReplicateEventListener.html[ReplicateEventListener] +save-update,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/SaveOrUpdateEventListener.html[SaveOrUpdateEventListener] +save,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/SaveOrUpdateEventListener.html[SaveOrUpdateEventListener] +update,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/SaveOrUpdateEventListener.html[SaveOrUpdateEventListener] +pre-load,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PreLoadEventListener.html[PreLoadEventListener] +pre-update,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PreUpdateEventListener.html[PreUpdateEventListener] +pre-delete,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PreDeleteEventListener.html[PreDeleteEventListener] +pre-insert,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PreInsertEventListener.html[PreInsertEventListener] +pre-collection-recreate,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PreCollectionRecreateEventListener.html[PreCollectionRecreateEventListener] +pre-collection-remove,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PreCollectionRemoveEventListener.html[PreCollectionRemoveEventListener] +pre-collection-update,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PreCollectionUpdateEventListener.html[PreCollectionUpdateEventListener] +post-load,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostLoadEventListener.html[PostLoadEventListener] +post-update,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostUpdateEventListener.html[PostUpdateEventListener] +post-delete,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostDeleteEventListener.html[PostDeleteEventListener] +post-insert,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostInsertEventListener.html[PostInsertEventListener] +post-commit-update,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostUpdateEventListener.html[PostUpdateEventListener] +post-commit-delete,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostDeleteEventListener.html[PostDeleteEventListener] +post-commit-insert,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostInsertEventListener.html[PostInsertEventListener] +post-collection-recreate,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostCollectionRecreateEventListener.html[PostCollectionRecreateEventListener] +post-collection-remove,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostCollectionRemoveEventListener.html[PostCollectionRemoveEventListener] +post-collection-update,https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/event/spi/PostCollectionUpdateEventListener.html[PostCollectionUpdateEventListener] +|=== + +For example, you could register a class `AuditEventListener` which implements `PostInsertEventListener`, `PostUpdateEventListener`, and `PostDeleteEventListener` using the following in an application: + +[source,groovy] +---- +beans = { + + auditListener(AuditEventListener) + + hibernateEventListeners(HibernateEventListeners) { + listenerMap = ['post-insert': auditListener, + 'post-update': auditListener, + 'post-delete': auditListener] + } +} +---- + +or use this in a plugin: + +[source,groovy] +---- +def doWithSpring = { + + auditListener(AuditEventListener) + + hibernateEventListeners(HibernateEventListeners) { + listenerMap = ['post-insert': auditListener, + 'post-update': auditListener, + 'post-delete': auditListener] + } +} +---- + + +==== Automatic timestamping + + +If you define a `dateCreated` property it will be set to the current date for you when you create new instances. Likewise, if you define a `lastUpdated` property it will be automatically be updated for you when you change persistent instances. + +If this is not the behaviour you want you can disable this feature with: + +[source,java] +---- +class Person { + Date dateCreated + Date lastUpdated + static mapping = { + autoTimestamp false + } +} +---- + +WARNING: If you have `nullable: false` constraints on either `dateCreated` or `lastUpdated`, your domain instances will fail validation - probably not what you want. Omit constraints from these properties unless you disable automatic timestamping. + +It is also possible to disable the automatic timestamping temporarily. This is most typically done in the case of a test where you need to define values for the `dateCreated` or `lastUpdated` in the past. It may also be useful for importing old data from other systems where you would like to keep the current values of the timestamps. + +Timestamps can be temporarily disabled for all domains, a specified list of domains, or a single domain. To get started, you need to get a reference to the `AutoTimestampEventListener`. If you already have access to the datastore, you can execute the `getAutoTimestampEventListener` method. If you don't have access to the datastore, inject the `autoTimestampEventListener` bean. + +Once you have a reference to the event listener, you can execute `withoutDateCreated`, `withoutLastUpdated`, or `withoutTimestamps`. The `withoutTimestamps` method will temporarily disable both `dateCreated` and `lastUpdated`. + +Example: + +[source,groovy] +---- +//Only the dateCreated property handling will be disabled for only the Foo domain +autoTimestampEventListener.withoutDateCreated(Foo) { + new Foo(dateCreated: new Date() - 1).save(flush: true) +} + +//Only the lastUpdated property handling will be disabled for only the Foo and Bar domains +autoTimestampEventListener.withoutLastUpdated(Foo, Bar) { + new Foo(lastUpdated: new Date() - 1, bar: new Bar(lastUpdated: new Date() + 1)).save(flush: true) +} + +//All timestamp property handling will be disabled for all domains +autoTimestampEventListener.withoutTimestamps { + new Foo(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true) + new Bar(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true) + new FooBar(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true) +} +---- + +WARNING: Because the timestamp handling is only disabled for the duration of the closure, you must flush the session during the closure execution! diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl.adoc new file mode 100644 index 00000000000..4202fc18d0b --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl.adoc @@ -0,0 +1,47 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +GORM domain classes can be mapped onto many legacy schemas with an Object Relational Mapping DSL (domain specific language). The following sections takes you through what is possible with the ORM DSL. + +NOTE: None of this is necessary if you are happy to stick to the conventions defined by GORM for table names, column names and so on. You only needs this functionality if you need to tailor the way GORM maps onto legacy schemas or configures caching + +Custom mappings are defined using a static `mapping` block defined within your domain class: + +[source,java] +---- +class Person { + ... + static mapping = { + version false + autoTimestamp false + } +} +---- + +You can also configure global mappings in `application.groovy` (or an external config file) using this setting: + +[source,java] +---- +grails.gorm.default.mapping = { + version false + autoTimestamp false +} +---- + +It has the same syntax as the standard `mapping` block but it applies to all your domain classes! You can then override these defaults within the `mapping` block of a domain class. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/caching.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/caching.adoc new file mode 100644 index 00000000000..31778ddd1d4 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/caching.adoc @@ -0,0 +1,167 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== Setting up caching + + +https://www.hibernate.org/[Hibernate] features a second-level cache with a customizable cache provider. This needs to be configured in the `grails-app/conf/application.yml` file as follows: + +[source,groovy] +---- +hibernate: + cache: + use_second_level_cache: true + region: + factory_class: 'jcache' +---- + +and the `ehcache` dependency using the `jakarta` classifier needs added to build.gradle: + +[source,groovy] +---- + dependencies { + implementation 'org.ehcache:ehcache', { + capabilities { + requireCapability('org.ehcache:ehcache-jakarta') + } + } + } +---- + +You can customize any of these settings, for example to use a distributed caching mechanism. + +NOTE: For further reading on caching and in particular Hibernate's second-level cache, refer to the https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#caching[Hibernate documentation] on the subject. + + +===== Caching instances + + +Call the `cache` method in your mapping block to enable caching with the default settings: + +[source,java] +---- +class Person { + ... + static mapping = { + table 'people' + cache true + } +} +---- + +This will configure a 'read-write' cache that includes both lazy and non-lazy properties. You can customize this further: + +[source,java] +---- +class Person { + ... + static mapping = { + table 'people' + cache usage: 'read-only', include: 'non-lazy' + } +} +---- + + +===== Caching associations + + +As well as the ability to use Hibernate's second level cache to cache instances you can also cache collections (associations) of objects. For example: + +[source,java] +---- +class Person { + + String firstName + + static hasMany = [addresses: Address] + + static mapping = { + table 'people' + version false + addresses column: 'Address', cache: true + } +} +---- + +[source,java] +---- +class Address { + String number + String postCode +} +---- + +This will enable a 'read-write' caching mechanism on the `addresses` collection. You can also use: + +[source,java] +---- +cache: 'read-write' // or 'read-only' or 'transactional' +---- + +to further configure the cache usage. + + +===== Caching Queries + +In order for the results of queries to be cached, you must enable caching in your mapping: + +[source,groovy] +---- +hibernate: + cache: + use_query_cache: true +---- + +To enable query caching for all queries created by dynamic finders, GORM etc. you can specify: + +[source,groovy] +---- +hibernate: + cache: + queries: true # This implicitly sets `use_query_cache=true` +---- + +You can cache queries such as dynamic finders and criteria. To do so using a dynamic finder you can pass the `cache` argument: + +[source,java] +---- +def person = Person.findByFirstName("Fred", [cache: true]) +---- + +You can also cache criteria queries: + +[source,java] +---- +def people = Person.withCriteria { + like('firstName', 'Fr%') + cache true +} +---- + + +===== Cache usages + + +Below is a description of the different cache settings and their usages: + +* `read-only` - If your application needs to read but never modify instances of a persistent class, a read-only cache may be used. +* `read-write` - If the application needs to update data, a read-write cache might be appropriate. +* `nonstrict-read-write` - If the application only occasionally needs to update data (i.e. if it is very unlikely that two transactions would try to update the same item simultaneously) and strict transaction isolation is not required, a `nonstrict-read-write` cache might be appropriate. +* `transactional` - The `transactional` cache strategy provides support for fully transactional cache providers such as JBoss TreeCache. Such a cache may only be used in a JTA environment and you must specify `hibernate.transaction.manager_lookup_class` in the `grails-app/conf/application.groovy` file's `hibernate` config. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/compositePrimaryKeys.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/compositePrimaryKeys.adoc new file mode 100644 index 00000000000..1f2e2215ef6 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/compositePrimaryKeys.adoc @@ -0,0 +1,88 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +GORM supports the concept of composite identifiers (identifiers composed from 2 or more properties). It is not an approach we recommend, but is available to you if you need it: + +[source,groovy] +---- +import org.apache.commons.lang.builder.HashCodeBuilder + +class Person implements Serializable { + + String firstName + String lastName + + boolean equals(other) { + if (!(other instanceof Person)) { + return false + } + + other.firstName == firstName && other.lastName == lastName + } + + int hashCode() { + def builder = new HashCodeBuilder() + builder.append firstName + builder.append lastName + builder.toHashCode() + } + + static mapping = { + id composite: ['firstName', 'lastName'] + } +} +---- + +The above will create a composite id of the `firstName` and `lastName` properties of the Person class. To retrieve an instance by id you use a prototype of the object itself: + +[source,java] +---- +def p = Person.get(new Person(firstName: "Fred", lastName: "Flintstone")) +println p.firstName +---- + +Domain classes mapped with composite primary keys must implement the `Serializable` interface and override the `equals` and `hashCode` methods, using the properties in the composite key for the calculations. The example above uses a `HashCodeBuilder` for convenience but it's fine to implement it yourself. + +Another important consideration when using composite primary keys is associations. If for example you have a many-to-one association where the foreign keys are stored in the associated table then 2 columns will be present in the associated table. + +For example consider the following domain class: + +[source,groovy] +---- +class Address { + Person person +} +---- + +In this case the `address` table will have an additional two columns called `person_first_name` and `person_last_name`. If you wish the change the mapping of these columns then you can do so using the following technique: + +[source,groovy] +---- +class Address { + Person person + static mapping = { + columns { + person { + column name: "FirstName" + column name: "LastName" + } + } + } +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customCascadeBehaviour.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customCascadeBehaviour.adoc new file mode 100644 index 00000000000..945bba74ca6 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customCascadeBehaviour.adoc @@ -0,0 +1,58 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +As described in the section on <>, the primary mechanism to control the way updates and deletes cascade from one association to another is the static <> property. + +However, the ORM DSL gives you complete access to Hibernate's https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#associations[transitive persistence] capabilities using the `cascade` attribute. + +Valid settings for the cascade attribute include: + +* `merge` - merges the state of a detached association +* `save-update` - cascades only saves and updates to an association +* `delete` - cascades only deletes to an association +* `lock` - useful if a pessimistic lock should be cascaded to its associations +* `refresh` - cascades refreshes to an association +* `evict` - cascades evictions (equivalent to `discard()` in GORM) to associations if set +* `all` - cascade _all_ operations to associations +* `all-delete-orphan` - Applies only to one-to-many associations and indicates that when a child is removed from an association then it should be automatically deleted. Children are also deleted when the parent is. + + +To specify the cascade attribute simply define one or more (comma-separated) of the aforementioned settings as its value: + +[source,java] +---- +class Person { + + String firstName + + static hasMany = [addresses: Address] + + static mapping = { + addresses cascade: "all-delete-orphan" + } +} +---- + +[source,java] +---- +class Address { + String street + String postCode +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customHibernateTypes.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customHibernateTypes.adoc new file mode 100644 index 00000000000..07d9094308e --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customHibernateTypes.adoc @@ -0,0 +1,89 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +You saw in an earlier section that you can use composition (with the `embedded` property) to break a table into multiple objects. You can achieve a similar effect with Hibernate's custom user types. These are not domain classes themselves, but plain Java or Groovy classes. Each of these types also has a corresponding "meta-type" class that implements https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/usertype/UserType.html[org.hibernate.usertype.UserType]. + +The https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#_custom_type[Hibernate reference manual] has some information on custom types, but here we will focus on how to map them in GORM. Let's start by taking a look at a simple domain class that uses an old-fashioned (pre-Java 1.5) type-safe enum class: + +[source,groovy] +---- +class Book { + + String title + String author + Rating rating + + static mapping = { + rating type: RatingUserType + } +} +---- + +All we have done is declare the `rating` field the enum type and set the property's type in the custom mapping to the corresponding `UserType` implementation. That's all you have to do to start using your custom type. If you want, you can also use the other column settings such as "column" to change the column name and "index" to add it to an index. + +Custom types aren't limited to just a single column - they can be mapped to as many columns as you want. In such cases you explicitly define in the mapping what columns to use, since Hibernate can only use the property name for a single column. Fortunately, GORM lets you map multiple columns to a property using this syntax: + +[source,java] +---- +class Book { + + String title + Name author + Rating rating + + static mapping = { + author type: NameUserType, { + column name: "first_name" + column name: "last_name" + } + rating type: RatingUserType + } +} +---- + +The above example will create "first_name" and "last_name" columns for the `author` property. You'll be pleased to know that you can also use some of the normal column/property mapping attributes in the column definitions. For example: + +[source,java] +---- +column name: "first_name", index: "my_idx", unique: true +---- + +The column definitions do _not_ support the following attributes: `type`, `cascade`, `lazy`, `cache`, and `joinTable`. + +One thing to bear in mind with custom types is that they define the _SQL types_ for the corresponding database columns. That helps take the burden of configuring them yourself, but what happens if you have a legacy database that uses a different SQL type for one of the columns? In that case, override the column's SQL type using the `sqlType` attribute: + +[source,java] +---- +class Book { + + String title + Name author + Rating rating + + static mapping = { + author type: NameUserType, { + column name: "first_name", sqlType: "text" + column name: "last_name", sqlType: "text" + } + rating type: RatingUserType, sqlType: "text" + } +} +---- + +Mind you, the SQL type you specify needs to still work with the custom type. So overriding a default of "varchar" with "text" is fine, but overriding "text" with "yes_no" isn't going to work. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customNamingStrategy.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customNamingStrategy.adoc new file mode 100644 index 00000000000..24fc20af883 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customNamingStrategy.adoc @@ -0,0 +1,81 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +By default GORM uses Hibernate's `ImprovedNamingStrategy` to convert domain class Class and field names to SQL table and column names by converting from camel-cased Strings to ones that use underscores as word separators. You can customize these on a per-class basis in the `mapping` closure but if there's a consistent pattern you can specify a different `NamingStrategy` class to use. + +Configure the class name to be used in `grails-app/conf/application.groovy` in the `hibernate` section, e.g. + +[source,java] +---- +dataSource { + pooled = true + dbCreate = "create-drop" + ... +} + +hibernate { + cache.use_second_level_cache = true + ... + naming_strategy = com.myco.myproj.CustomNamingStrategy +} +---- + +You can also specify the name of the class and it will be loaded for you: + +[source,java] +---- +hibernate { + ... + naming_strategy = 'com.myco.myproj.CustomNamingStrategy' +} +---- + +A third option is to provide an instance if there is some configuration required beyond calling the default constructor: + +[source,java] +---- +hibernate { + ... + def strategy = new com.myco.myproj.CustomNamingStrategy() + // configure as needed + naming_strategy = strategy +} +---- + +You can use an existing class or write your own, for example one that prefixes table names and column names: + +[source,java] +---- +package com.myco.myproj + +import org.hibernate.cfg.ImprovedNamingStrategy +import org.hibernate.util.StringHelper + +class CustomNamingStrategy extends ImprovedNamingStrategy { + + String classToTableName(String className) { + "table_" + StringHelper.unqualify(className) + } + + String propertyToColumnName(String propertyName) { + "col_" + StringHelper.unqualify(propertyName) + } +} +---- + diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/databaseIndices.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/databaseIndices.adoc new file mode 100644 index 00000000000..42da40092fd --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/databaseIndices.adoc @@ -0,0 +1,37 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +To get the best performance out of your queries it is often necessary to tailor the table index definitions. How you tailor them is domain specific and a matter of monitoring usage patterns of your queries. With GORM's DSL you can specify which columns are used in which indexes: + +[source,java] +---- +class Person { + String firstName + String address + static mapping = { + table 'people' + version false + id column: 'person_id' + firstName column: 'First_Name', index: 'Name_Idx' + address column: 'Address', index: 'Name_Idx,Address_Index' + } +} +---- + +Note that you cannot have any spaces in the value of the `index` attribute; in this example `index:'Name_Idx, Address_Index'` will cause an error. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/derivedProperties.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/derivedProperties.adoc new file mode 100644 index 00000000000..0f61e2a399b --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/derivedProperties.adoc @@ -0,0 +1,96 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +A derived property is one that takes its value from a SQL expression, often but not necessarily based on the value of one or more other persistent properties. Consider a Product class like this: + +[source,java] +---- +class Product { + Float price + Float taxRate + Float tax +} +---- + +If the `tax` property is derived based on the value of `price` and `taxRate` properties then is probably no need to persist the `tax` property. The SQL used to derive the value of a derived property may be expressed in the ORM DSL like this: + +[source,java] +---- +class Product { + Float price + Float taxRate + Float tax + + static mapping = { + tax formula: 'PRICE * TAX_RATE' + } +} +---- + +Note that the formula expressed in the ORM DSL is SQL so references to other properties should relate to the persistence model not the object model, which is why the example refers to `PRICE` and `TAX_RATE` instead of `price` and `taxRate`. + +With that in place, when a Product is retrieved with something like `Product.get(42)`, the SQL that is generated to support that will look something like this: + +[source,groovy] +---- +select + product0_.id as id1_0_, + product0_.version as version1_0_, + product0_.price as price1_0_, + product0_.tax_rate as tax4_1_0_, + product0_.PRICE * product0_.TAX_RATE as formula1_0_ +from + product product0_ +where + product0_.id=? +---- + +Since the `tax` property is derived at runtime and not stored in the database it might seem that the same effect could be achieved by adding a method like `getTax()` to the `Product` class that simply returns the product of the `taxRate` and `price` properties. With an approach like that you would give up the ability query the database based on the value of the `tax` property. Using a derived property allows exactly that. To retrieve all `Product` objects that have a `tax` value greater than 21.12 you could execute a query like this: + +[source,java] +---- +Product.findAllByTaxGreaterThan(21.12) +---- + +Derived properties may be referenced in the Criteria API: + +[source,java] +---- +Product.withCriteria { + gt 'tax', 21.12f +} +---- + +The SQL that is generated to support either of those would look something like this: + +[source,groovy] +---- +select + this_.id as id1_0_, + this_.version as version1_0_, + this_.price as price1_0_, + this_.tax_rate as tax4_1_0_, + this_.PRICE * this_.TAX_RATE as formula1_0_ +from + product this_ +where + this_.PRICE * this_.TAX_RATE>? +---- + +NOTE: Because the value of a derived property is generated in the database and depends on the execution of SQL code, derived properties may not have GORM constraints applied to them. If constraints are specified for a derived property, they will be ignored. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/fetchingDSL.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/fetchingDSL.adoc new file mode 100644 index 00000000000..a8e18579d48 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/fetchingDSL.adoc @@ -0,0 +1,195 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + + +===== Lazy Collections + + +As discussed in the section on <>, GORM collections are lazily loaded by default but you can change this behaviour with the ORM DSL. There are several options available to you, but the most common ones are: + +* lazy: false +* fetch: 'join' + +and they're used like this: + +[source,java] +---- +class Person { + + String firstName + Pet pet + + static hasMany = [addresses: Address] + + static mapping = { + addresses lazy: false + pet fetch: 'join' + } +} +---- + +[source,java] +---- +class Address { + String street + String postCode +} +---- + +[source,java] +---- +class Pet { + String name +} +---- + +The first option, `lazy: false` , ensures that when a `Person` instance is loaded, its `addresses` collection is loaded at the same time with a second SELECT. The second option is basically the same, except the collection is loaded with a JOIN rather than another SELECT. Typically you want to reduce the number of queries, so `fetch: 'join'` is the more appropriate option. On the other hand, it could feasibly be the more expensive approach if your domain model and data result in more and larger results than would otherwise be necessary. + +For more advanced users, the other settings available are: + +* `batchSize: N` +* `lazy: false, batchSize: N` + +where N is an integer. These let you fetch results in batches, with one query per batch. As a simple example, consider this mapping for `Person`: + +[source,groovy] +---- +class Person { + + String firstName + Pet pet + + static mapping = { + pet batchSize: 5 + } +} +---- +If a query returns multiple `Person` instances, then when we access the first `pet` property, Hibernate will fetch that `Pet` plus the four next ones. You can get the same behaviour with eager loading by combining `batchSize` with the `lazy: false` option. + +You can find out more about these options in the https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#fetching[Hibernate user guide]. Note that ORM DSL does not currently support the "subselect" fetching strategy. + + +===== Lazy Single-Ended Associations + + +In GORM, one-to-one and many-to-one associations are by default lazy. Non-lazy single ended associations can be problematic when you load many entities because each non-lazy association will result in an extra SELECT statement. If the associated entities also have non-lazy associations, the number of queries grows significantly! + +Use the same technique as for lazy collections to make a one-to-one or many-to-one association non-lazy/eager: + +[source,java] +---- +class Person { + String firstName +} +---- + +[source,java] +---- +class Address { + + String street + String postCode + + static belongsTo = [person: Person] + + static mapping = { + person lazy: false + } +} +---- + +Here we configure GORM to load the associated `Person` instance (through the `person` property) whenever an `Address` is loaded. + + +===== Lazy Associations and Proxies + + +Hibernate uses runtime-generated proxies to facilitate single-ended lazy associations; Hibernate dynamically subclasses the entity class to create the proxy. + +Consider the previous example but with a lazily-loaded `person` association: Hibernate will set the `person` property to a proxy that is a subclass of `Person`. When you call any of the getters (except for the `id` property) or setters on that proxy, Hibernate will load the entity from the database. + +Unfortunately this technique can produce surprising results. Consider the following example classes: + +[source,java] +---- +class Pet { + String name +} +---- + +[source,java] +---- +class Dog extends Pet { +} +---- + +[source,java] +---- +class Person { + String name + Pet pet +} +---- + +Proxies can have confusing behavior when combined with inheritance. Because the proxy is only a subclass of the parent class, any attempt to cast or access data on the subclass will fail. Assuming we have a single `Person` instance with a `Dog` as the `pet`. + +The code below will not fail because directly querying the `Pet` table does not require the resulting objects to be proxies because they are not lazy. + +[source,groovy] +---- +def pet = Pet.get(1) +assert pet instanceof Dog +---- + +The following code will fail because the association is lazy and the `pet` instance is a proxy. + +[source,groovy] +---- +def person = Person.get(1) +assert person.pet instanceof Dog +---- + +If the only goal is to check if the proxy is an instance of a class, there is one helper method available to do so that works with proxies. Take special care in using it though because it does cause a call to the database to retrieve the association data. + +[source,groovy] +---- +def person = Person.get(1) +assert person.pet.instanceOf(Dog) +---- + +There are a couple of ways to approach this issue. The first rule of thumb is that if it is known ahead of time that the association data is required, join the data in the query of the `Person`. For example, the following assertion is true. + +[source,groovy] +---- +def person = Person.where { id == 1 }.join("pet").get() +assert person.pet instanceof Dog +---- + +In the above example the `pet` association is no longer lazy because it is being retrieved along with the `Person` and thus no proxies are necessary. There are cases when it makes sense for a proxy to be returned, mostly in the case where its impossible to know if the data will be used or not. For those cases in order to access properties of the subclasses, the proxy must be unwrapped. To unwrap a proxy inject an instance of link:../api/org/grails/datastore/mapping/proxy/ProxyHandler.html[ProxyHandler] and pass the proxy to the `unwrap` method. + +[source,groovy] +---- +def person = Person.get(1) +assert proxyHandler.unwrap(person.pet) instanceof Dog +---- + +For cases where dependency injection is impractical or not available, a helper method link:../api/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.html#unwrapIfProxy(java.lang.Object)[GrailsHibernateUtil.unwrapIfProxy(Object)] can be used instead. + +Unwrapping a proxy is different than initializing it. Initializing a proxy simply populates the underlying instance with data from the database, however unwrapping a returns the inner target. + diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/identity.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/identity.adoc new file mode 100644 index 00000000000..b1ae2f3d65c --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/identity.adoc @@ -0,0 +1,55 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +You can customize how GORM generates identifiers for the database using the DSL. By default GORM relies on the native database mechanism for generating ids. This is by far the best approach, but there are still many schemas that have different approaches to identity. + +To deal with this Hibernate defines the concept of an id generator. You can customize the id generator and the column it maps to as follows: + +[source,java] +---- +class Person { + ... + static mapping = { + table 'people' + version false + id generator: 'hilo', + params: [table: 'hi_value', + column: 'next_value', + max_lo: 100] + } +} +---- + +In this case we're using one of Hibernate's built in 'hilo' generators that uses a separate table to generate ids. + +NOTE: For more information on the different Hibernate generators refer to the https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#identifiers-generators[Hibernate reference documentation] + +Although you don't typically specify the `id` field (GORM adds it for you) you can still configure its mapping like the other properties. For example to customise the column for the id property you can do: + +[source,java] +---- +class Person { + ... + static mapping = { + table 'people' + version false + id column: 'person_id' + } +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/inheritanceStrategies.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/inheritanceStrategies.adoc new file mode 100644 index 00000000000..c5ceb92bb43 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/inheritanceStrategies.adoc @@ -0,0 +1,37 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +By default GORM classes use `table-per-hierarchy` inheritance mapping. This has the disadvantage that columns cannot have a `NOT-NULL` constraint applied to them at the database level. If you would prefer to use a `table-per-subclass` inheritance strategy you can do so as follows: + +[source,java] +---- +class Payment { + Integer amount + + static mapping = { + tablePerHierarchy false + } +} + +class CreditCardPayment extends Payment { + String cardNumber +} +---- + +The mapping of the root `Payment` class specifies that it will not be using `table-per-hierarchy` mapping for all child classes. \ No newline at end of file diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/optimisticLockingAndVersioning.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/optimisticLockingAndVersioning.adoc new file mode 100644 index 00000000000..560804af50d --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/optimisticLockingAndVersioning.adoc @@ -0,0 +1,58 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +As discussed in the section on <>, by default GORM uses optimistic locking and automatically injects a `version` property into every class which is in turn mapped to a `version` column at the database level. + +If you're mapping to a legacy schema that doesn't have version columns (or there's some other reason why you don't want/need this feature) you can disable this with the `version` method: + +[source,java] +---- +class Person { + ... + static mapping = { + table 'people' + version false + } +} +---- + +NOTE: If you disable optimistic locking you are essentially on your own with regards to concurrent updates and are open to the risk of users losing data (due to data overriding) unless you use <> + + +===== Version columns types + + +By default GORM maps the `version` property as a `Long` that gets incremented by one each time an instance is updated. But Hibernate also supports using a `Timestamp`, for example: + +[source,java] +---- +import java.sql.Timestamp + +class Person { + + ... + Timestamp version + + static mapping = { + table 'people' + } +} +---- + +There's a slight risk that two updates occurring at nearly the same time on a fast server can end up with the same timestamp value but this risk is very low. One benefit of using a `Timestamp` instead of a `Long` is that you combine the optimistic locking and last-updated semantics into a single column. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/tableAndColumnNames.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/tableAndColumnNames.adoc new file mode 100644 index 00000000000..abc2e6e5a05 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/tableAndColumnNames.adoc @@ -0,0 +1,219 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + + +===== Table names + + +The database table name which the class maps to can be customized using the `table` method: + +[source,java] +---- +class Person { + ... + static mapping = { + table 'people' + } +} +---- + +In this case the class would be mapped to a table called `people` instead of the default name of `person`. + + +===== Column names + + +It is also possible to customize the mapping for individual columns onto the database. For example to change the name you can do: + +[source,java] +---- +class Person { + + String firstName + + static mapping = { + table 'people' + firstName column: 'First_Name' + } +} +---- + +Here `firstName` is a dynamic method within the `mapping` Closure that has a single Map parameter. Since its name corresponds to a domain class persistent field, the parameter values (in this case just `"column"`) are used to configure the mapping for that property. + + +===== Column type + + +GORM supports configuration of Hibernate types with the DSL using the type attribute. This includes specifying user types that implement the Hibernate https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/usertype/UserType.html[org.hibernate.usertype.UserType] interface, which allows complete customization of how a type is persisted. As an example if you had a `PostCodeType` you could use it as follows: + +[source,java] +---- +class Address { + + String number + String postCode + + static mapping = { + postCode type: PostCodeType + } +} +---- + +Alternatively if you just wanted to map it to one of Hibernate's basic types other than the default chosen by GORM you could use: + +[source,java] +---- +class Address { + + String number + String postCode + + static mapping = { + postCode type: 'text' + } +} +---- + +This would make the `postCode` column map to the default large-text type for the database you're using (for example TEXT or CLOB). + +See the Hibernate documentation regarding https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#basic-explicit[Basic Types] for further information. + + +===== Many-to-One/One-to-One Mappings + + +In the case of associations it is also possible to configure the foreign keys used to map associations. In the case of a many-to-one or one-to-one association this is exactly the same as any regular column. For example consider the following: + +[source,java] +---- +class Person { + + String firstName + Address address + + static mapping = { + table 'people' + firstName column: 'First_Name' + address column: 'Person_Address_Id' + } +} +---- + +By default the `address` association would map to a foreign key column called `address_id`. By using the above mapping we have changed the name of the foreign key column to `Person_Adress_Id`. + +===== One-to-Many Mapping + +With a bidirectional one-to-many you can change the foreign key column used by changing the column name on the many side of the association as per the example in the previous section on one-to-one associations. However, with unidirectional associations the foreign key needs to be specified on the association itself. For example given a unidirectional one-to-many relationship between `Person` and `Address` the following code will change the foreign key in the `address` table: + +[source,java] +---- +class Person { + + String firstName + + static hasMany = [addresses: Address] + + static mapping = { + table 'people' + firstName column: 'First_Name' + addresses column: 'Person_Address_Id' + } +} +---- + +If you don't want the column to be in the `address` table, but instead some intermediate join table you can use the `joinTable` parameter: + +[source,java] +---- +class Person { + + String firstName + + static hasMany = [addresses: Address] + + static mapping = { + table 'people' + firstName column: 'First_Name' + addresses joinTable: [name: 'Person_Addresses', + key: 'Person_Id', + column: 'Address_Id'] + } +} +---- + + +===== Many-to-Many Mapping + + +GORM, by default maps a many-to-many association using a join table. For example consider this many-to-many association: + +[source,java] +---- +class Group { + ... + static hasMany = [people: Person] +} +---- + +[source,java] +---- +class Person { + ... + static belongsTo = Group + static hasMany = [groups: Group] +} +---- + +In this case GORM will create a join table called `group_person` containing foreign keys called `person_id` and `group_id` referencing the `person` and `group` tables. To change the column names you can specify a column within the mappings for each class. + +[source,java] +---- +class Group { + ... + static mapping = { + people column: 'Group_Person_Id' + } +} +class Person { + ... + static mapping = { + groups column: 'Group_Group_Id' + } +} +---- + +You can also specify the name of the join table to use: + +[source,java] +---- +class Group { + ... + static mapping = { + people column: 'Group_Person_Id', + joinTable: 'PERSON_GROUP_ASSOCIATIONS' + } +} +class Person { + ... + static mapping = { + groups column: 'Group_Group_Id', + joinTable: 'PERSON_GROUP_ASSOCIATIONS' + } +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/configurationDefaults.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/configurationDefaults.adoc new file mode 100644 index 00000000000..a5800c8fcbd --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/configurationDefaults.adoc @@ -0,0 +1,59 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +The `grails.gorm.default.mapping` and `grails.gorm.default.constraints` settings deserve special mention. These define the default <> and the default <> used by each entity. + +==== Altering the Default Database Mapping + +You may have reason to want to change how all domain classes map to the database. For example, by default GORM uses the `native` id generation strategy of the database, whether that be an auto-increment column or a sequence. + +If you wish to globally change all domain classes to use a `uuid` strategy then you can specify that in the default mapping: + +[source,groovy] +.grails-app/conf/application.groovy +---- +grails.gorm.default.mapping = { + cache true + id generator:'uuid' +} +---- + +As you can see you can assign a closure that is equivalent to the `mapping` block used to <>. + +NOTE: Because the setting is Groovy configuration it must go into a Groovy-aware configuration format. This can be `grails-app/conf/application.groovy` in Grails, or `src/main/resources/application.groovy` in Spring Boot. + +==== Altering the Default Constraints + +For validation, GORM applies a default set of <> to all domain classes. + +For example, by default all properties of GORM classes are not nullable by default. This means a value has to be supplied for each property, otherwise you will get a validation error. + +In most cases this is what you want, but if you are dealing with a large number of columns, it may prove inconvinient. + +You can alter the default constraints using Groovy configuration using the `grails.gorm.default.constraints` setting: + +[source,groovy] +.grails-app/conf/application.groovy +---- +grails.gorm.default.constraints = { + '*'(nullable: true, size: 1..20) +} +---- + +In the above example, all properties are allowed to be `nullable` by default, but limited to a size of between 1 and 20. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/configurationReference.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/configurationReference.adoc new file mode 100644 index 00000000000..2eed1a22456 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/configurationReference.adoc @@ -0,0 +1,67 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +You can refer to the link:../api/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettings.html[HibernateConnectionSourceSettings] class for all available configuration options, but below is a table of the common ones: + +[format="csv", options="header"] +|=== +name,description,default value +`grails.gorm.flushMode`, The flush mode to use, `COMMIT` +`grails.gorm.failOnError`, Whether to throw an exception on validation error, `false` +`grails.gorm.default.mapping`,The default mapping to apply to all classes, `null` +`grails.gorm.default.constraints`,The default constraints to apply to all classes, `null` +`grails.gorm.multiTenancy.mode`,The multi tenancy mode, `NONE` +|=== + +The following are common configuration options for the SQL connection: + +[format="csv", options="header"] +|=== +name,description,default value +`dataSource.url`, The JDBC url, `jdbc:h2:mem:grailsDB` +`dataSource.driverClassName`, The class of the JDBC driver, detected from URL +`dataSource.username`, The JDBC username, `null` +`dataSource.password`, The JDBC password, `null` +`dataSource.jndiName`, The name of the JNDI resource for the `DataSource`, `null` +`dataSource.pooled`, Whether the connection is pooled, `true` +`dataSource.lazy`, Whether a `LazyConnectionDataSourceProxy` should be used, `true` +`dataSource.transactionAware`, Whether a `TransactionAwareDataSourceProxy` should be used, `true` +`dataSource.readOnly`, Whether the DataSource is read-only, `false` +`dataSource.options`, A map of options to pass to the underlying JDBC driver, `null` +|=== + +And the following are common configuration options for Hibernate: + +[format="csv", options="header"] +|=== +name,description,default value +`hibernate.dialect`, The hibernate dialect to use, detected automatically from DataSource +`hibernate.readOnly`, Whether Hibernate should be read-only, `false` +`hibernate.configClass`, The configuration class to use, `HibernateMappingContextConfiguration` +`hibernate.hbm2ddl.auto`, Whether to create the tables on startup, `none` +`hibernate.use_second_level_cache`, Whether to use the second level cache, `true` +`hibernate.cache.queries`, Whether to cache queries (see Caching Queries), `false` +`hibernate.cache.use_query_cache`, Enables the query cache, `false` +`hibernate.configLocations`, Location of additional Hibernate XML configuration files +`hibernate.packagesToScan`, Specify packages to search for autodetection of your entity classes in the classpath +|=== + +In addition, any additional settings that start with `hibernate.` are passed through to Hibernate, so if there is any specific feature of Hibernate you wish to configure that is possible. + +TIP: The above table covers the common configuration options. For all configuration refer to properties of the link:../api/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettings.html[HibernateConnectionSourceSettings] class. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/hibernateCustomization.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/hibernateCustomization.adoc new file mode 100644 index 00000000000..2e0d41dcce3 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/hibernateCustomization.adoc @@ -0,0 +1,48 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +If you want to hook into GORM and customize how Hibernate is configured there are a variety of ways to achieve that when using GORM. + +Firstly, as mentioned previously, any configuration you specify when configuring GORM for Hibernate will be passed through to Hibernate so you can configure any setting of Hibernate itself. + +For more advanced configuration you may want to configure or supply a new link:../api/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactory.html[HibernateConnectionSourceFactory] instance or a link:../api/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.html[HibernateMappingContextConfiguration] or both. + +==== The HibernateConnectionSourceFactory + +The `HibernateConnectionSourceFactory` is used to create a new Hibernate `SessionFactory` on startup. + +If you are using Spring, it is registered as a Spring bean using the name `hibernateConnectionSourceFactory` and therefore can be overridden. + +If you are not using Spring it can be passed to the constructor of the `HibernateDatastore` class on instantiation. + +The `HibernateConnectionSourceFactory` has a few useful setters that allow you to specify a Hibernate https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/Interceptor.html[Interceptor] or https://docs.jboss.org/hibernate/orm/5.6/javadocs/org/hibernate/boot/spi/MetadataContributor.html[MetadataContributor] (Hibernate 5+ only). + +==== The HibernateMappingContextConfiguration + +link:../api/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.html[HibernateMappingContextConfiguration] is built by the `HibernateConnectionSourceFactory`, but a customized version can be specified using the `hibernate.configClass` setting in your configuration: + +[source,yaml] +.grails-app/conf/application.yml +---- +hibernate: + configClass: com.example.MyHibernateMappingContextConfiguration +---- + +The customized version should extend `HibernateMappingContextConfiguration` and using this class you can add additional classes, packages, `hbm.cfg.xml` files and so on. + diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/index.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/index.adoc new file mode 100644 index 00000000000..bdcf576f592 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/configuration/index.adoc @@ -0,0 +1,59 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +GORM for Hibernate can be configured with the `grails-app/conf/application.yml` file when using Grails, the `src/main/resources/application.yml` file when using Spring Boot or by passing a `Map` or instanceof the `PropertyResolver` interface to the `org.grails.orm.hibernate.HibernateDatastore` class when used standalone. + +All configuration options are read and materialized into an instance of link:../api/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettings.html[HibernateConnectionSourceSettings]. + +=== Configuration Example + +If you are using Grails or Spring Boot, the following is an example of configuration specified in `application.yml`: + +[source,yaml] +---- +dataSource: + pooled: true + dbCreate: create-drop + url: jdbc:h2:mem:devDb + driverClassName: org.h2.Driver + username: sa + password: +hibernate: + cache: + queries: false + use_second_level_cache: true + use_query_cache: false + region.factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory +---- + +Each one of the settings under the `dataSource` block is set on the link:../api/org/grails/datastore/gorm/jdbc/connections/DataSourceSettings.html[DataSourceSettings] property of `HibernateConnectionSourceSettings`. + +Whilst each setting under the `hibernate` block is set on the link:../api/org/grails/orm/hibernate/connections/HibernateConnectionSourceSettings.HibernateSettings.html[HibernateSettings] property. + +=== Configuration Reference + +include::configurationReference.adoc[] + +=== The Default Mapping & Constraints + +include::configurationDefaults.adoc[] + +=== Hibernate Customization + +include::hibernateCustomization.adoc[] diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/applyingConstraints.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/applyingConstraints.adoc new file mode 100644 index 00000000000..aa3f80af8a0 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/applyingConstraints.adoc @@ -0,0 +1,154 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +Within a domain class constraints are defined with the constraints property that is assigned a code block: + +[source,java] +---- +class User { + String login + String password + String email + Integer age + + static constraints = { + ... + } +} +---- + +You then use method calls that match the property name for which the constraint applies in combination with named parameters to specify constraints: + +[source,java] +---- +class User { + ... + + static constraints = { + login size: 5..15, blank: false, unique: true + password size: 5..15, blank: false + email email: true, blank: false + age min: 18 + } +} +---- + +In this example we've declared that the `login` property must be between 5 and 15 characters long, it cannot be blank and must be unique. We've also applied other constraints to the `password`, `email` and `age` properties. + +NOTE: By default, all domain class properties are not nullable (i.e. they have an implicit `nullable: false` constraint). + +Note that constraints are only evaluated once which may be relevant for a constraint that relies on a value like an instance of `java.util.Date`. + +[source,java] +---- +class User { + ... + + static constraints = { + // this Date object is created when the constraints are evaluated, not + // each time an instance of the User class is validated. + birthDate max: new Date() + } +} +---- + + +=== Referencing Instances in Constraints + + +It's very easy to attempt to reference instance variables from the static constraints block, but this isn't legal in Groovy (or Java). If you do so, you will get a `MissingPropertyException` for your trouble. For example, you may try the following: + +[source,groovy] +---- +class Response { + Survey survey + Answer answer + + static constraints = { + survey blank: false + answer blank: false, inList: survey.answers + } +} +---- + +See how the `inList` constraint references the instance property `survey`? That won't work. Instead, use a custom `validator` constraint: + +[source,groovy] +---- +class Response { + ... + static constraints = { + survey blank: false + answer blank: false, validator: { val, Response obj -> val in obj.survey.answers } + } +} +---- + +In this example, the `obj` argument to the custom validator is the domain _instance_ that is being validated, so we can access its `survey` property and return a boolean to indicate whether the new value for the `answer` property, `val`, is valid. + +=== Cascade constraints validation + +If GORM entity references some other entities, then during its constraints evaluation (validation) the constraints of the referenced entity could be +evaluated also, if needed. There is a special parameter `cascadeValidate` in the entity mappings section, which manage the way of this _cascaded_ validation happens. + +[source,groovy] +---- +class Author { + Publisher publisher + + static mapping = { + publisher(cascadeValidate: "dirty") + } +} + +class Publisher { + String name + + static constraints = { + name blank: false + } +} +---- + +The following table presents all options, which can be used: +[cols="1,2"] +|=== +|Option |Description + +|none +|Will not do any cascade validation at all for the association. + +|default +|The DEFAULT option. GORM performs cascade validation in some cases. + +|dirty +|Only cascade validation if the referenced object is dirty via the `DirtyCheckable` trait. If the object doesn't implement DirtyCheckable, this will fall back to `default`. + +|owned +|Only cascade validation if the entity <> the referenced object. +|=== + +It is possible to set the global option for the `cascadeValidate`: +[source,groovy] +.grails-app/conf/application.groovy +---- +grails.gorm.default.mapping = { + '*'(cascadeValidate: 'dirty') +} +---- \ No newline at end of file diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/constraintReference.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/constraintReference.adoc new file mode 100644 index 00000000000..9142ff6bf40 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/constraintReference.adoc @@ -0,0 +1,43 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +The following table summarizes the available constraints with a brief example: + +[format="csv", options="header"] +|=== + +Constraint,Description,Example +blank,Validates that a String value is not blank,`login(blank:false)` +creditCard,Validates that a String value is a valid credit card number,`cardNumber(creditCard: true)` +email,Validates that a String value is a valid email address.,`homeEmail(email: true)` +inList,Validates that a value is within a range or collection of constrained values.,`name(inList: ["Joe"])` +matches,Validates that a String value matches a given regular expression.,`login(matches: "[a-zA-Z]+")` +max,Validates that a value does not exceed the given maximum value.,`age(max: new Date())` `price(max: 999F)` +maxSize,Validates that a value's size does not exceed the given maximum value.,`children(maxSize: 25)` +min,Validates that a value does not fall below the given minimum value.,`age(min: new Date())` `price(min: 0F)` +minSize,Validates that a value's size does not fall below the given minimum value.,`children(minSize: 25)` +notEqual,Validates that that a property is not equal to the specified value,`login(notEqual: "Bob")` +nullable,Allows a property to be set to `null` - defaults to `false`.,`age(nullable: true)` +range,Uses a Groovy range to ensure that a property's value occurs within a specified range,`age(range: 18..65)` +scale,Set to the desired scale for floating point numbers (i.e. the number of digits to the right of the decimal point).,`salary(scale: 2)` +size,Uses a Groovy range to restrict the size of a collection or number or the length of a String.,`children(size: 5..15)` +unique,Constrains a property as unique at the database level,`login(unique: true)` +url,Validates that a String value is a valid URL.,`homePage(url: true)` +validator,Adds custom validation to a field.,See documentation +|=== diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/gormConstraints.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/gormConstraints.adoc new file mode 100644 index 00000000000..0a595c0f2f2 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/gormConstraints.adoc @@ -0,0 +1,131 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +Although constraints are primarily for validation, it is important to understand that constraints can affect the way in which the database schema is generated. + +Where feasible, GORM uses a domain class's constraints to influence the database columns generated for the corresponding domain class properties. + +Consider the following example. Suppose we have a domain model with the following properties: + +[source,groovy] +---- +String name +String description +---- + +By default, in MySQL, GORM would define these columns as + +[format="csv", options="header"] +|=== + +Column,Data Type +name,varchar(255) +description,varchar(255) +|=== + +But perhaps the business rules for this domain class state that a description can be up to 1000 characters in length. If that were the case, we would likely define the column as follows _if_ we were creating the table with an SQL script. + +[format="csv", options="header"] +|=== + +Column,Data Type +description,TEXT +|=== + +Chances are we would also want to have some application-based validation to make sure we don't exceed that 1000 character limit _before_ we persist any records. In GORM, we achieve this validation with <>. We would add the following constraint declaration to the domain class. + +[source,groovy] +---- +static constraints = { + description maxSize: 1000 +} +---- + +This constraint would provide both the application-based validation we want and it would also cause the schema to be generated as shown above. Below is a description of the other constraints that influence schema generation. + + +==== Constraints Affecting String Properties + + +* `inList` +* `maxSize` +* `size` + +If either the `maxSize` or the `size` constraint is defined, Grails sets the maximum column length based on the constraint value. + +In general, it's not advisable to use both constraints on the same domain class property. However, if both the `maxSize` constraint and the `size` constraint are defined, then GORM sets the column length to the minimum of the `maxSize` constraint and the upper bound of the size constraint. (GORM uses the minimum of the two, because any length that exceeds that minimum will result in a validation error.) + +If the `inList` constraint is defined (and the `maxSize` and the `size` constraints are not defined), then GORM sets the maximum column length based on the length of the longest string in the list of valid values. For example, given a list including values "Java", "Groovy", and "C++", GORM would set the column length to 6 (i.e., the number of characters in the string "Groovy"). + + +==== Constraints Affecting Numeric Properties + + +* `min` +* `max` +* `range` + +If the `max`, `min`, or `range` constraint is defined, GORM attempts to set the column precision based on the constraint value. (The success of this attempted influence is largely dependent on how Hibernate interacts with the underlying DBMS.) + +In general, it's not advisable to combine the pair `min`/`max` and `range` constraints together on the same domain class property. However, if both of these constraints is defined, then GORM uses the minimum precision value from the constraints. (GORM uses the minimum of the two, because any length that exceeds that minimum precision will result in a validation error.) + +* `scale` + +If the scale constraint is defined, then GORM attempts to set the column <> based on the constraint value. This rule only applies to floating point numbers (i.e., `java.lang.Float`, `java.Lang.Double`, `java.lang.BigDecimal`, or subclasses of `java.lang.BigDecimal`). The success of this attempted influence is largely dependent on how Hibernate interacts with the underlying DBMS. + +The constraints define the minimum/maximum numeric values, and GORM derives the maximum number of digits for use in the precision. Keep in mind that specifying only one of `min`/`max` constraints will not affect schema generation (since there could be large negative value of property with max:100, for example), unless the specified constraint value requires more digits than default Hibernate column precision is (19 at the moment). For example: + +[source,groovy] +---- +someFloatValue max: 1000000, scale: 3 +---- + +would yield: + +[source,groovy] +---- +someFloatValue DECIMAL(19, 3) // precision is default +---- + +but + +[source,groovy] +---- +someFloatValue max: 12345678901234567890, scale: 5 +---- + +would yield: +[source,groovy] +---- +someFloatValue DECIMAL(25, 5) // precision = digits in max + scale +---- + +and + +[source,groovy] +---- +someFloatValue max: 100, min: -100000 +---- + +would yield: + +[source,groovy] +---- +someFloatValue DECIMAL(8, 2) // precision = digits in min + default scale +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/index.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/index.adoc new file mode 100644 index 00000000000..59ed9c4d000 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/constraints/index.adoc @@ -0,0 +1,32 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +Constraints are how you define validation when using GORM entities. + +=== Applying Constraints + +include::applyingConstraints.adoc[] + +=== Constraints Reference + +include::constraintReference.adoc[] + +=== Constraints and Database Mapping + +include::gormConstraints.adoc[] diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/configuration.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/configuration.adoc new file mode 100644 index 00000000000..1fd6283222d --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/configuration.adoc @@ -0,0 +1,68 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +=== Configuration + +There are a few configuration options for the plugin. All configurations are prefixed with `grails.plugin.databasemigration`: + +[options="header"] +|================================== +|Property |Default |Meaning +|changelogLocation |`grails-app/migrations` |the folder containing the main changelog file (which can include one or more other files) +|changelogFileName |`changelog.groovy` |the name of the main changelog file +|changelogProperties |none |a map of properties to use for property substitution in Groovy DSL changelogs +|contexts |none |A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be run +|dbDocLocation |`target/dbdoc` |the directory where the output from the <> script is written +|dbDocController.enabled |`true` in dev mode |whether the /dbdoc/ url is accessible at runtime +|dropOnStart |`false` |if `true` then drops all tables before auto-running migrations (if updateOnStart is true) +|updateOnStart |`false` |if `true` then changesets from the specified list of names will be run at startup +|updateOnStartFileName |none |the file name (relative to `changelogLocation`) to run at startup if `updateOnStart` is `true` +|updateOnStartDefaultSchema |none |the default schema to use when running auto-migrate on start +|updateOnStartContexts |none |A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be run +|updateAllOnStart |false |if `true` then changesets from the specified list of names will be run at startup for all dataSources. Useful for Grails Multitenancy with Multiple Databases (same db schema) +|autoMigrateScripts |['RunApp'] |the scripts when running auto-migrate. Useful to run auto-migrate during test phase with: ['RunApp', 'TestApp'] +|excludeObjects |none |A comma-delimited list of database object names to ignore while performing a dbm-gorm-diff or dbm-generate-gorm-changelog +|includeObjects |none |A comma-delimited list of database object names to look for while performing a dbm-gorm-diff or dbm-generate-gorm-changelog +|databaseChangeLogTableName |'databasechangelog' |the Liquibase changelog record table name +|databaseChangeLogLockTableName |'databasechangeloglock' |the Liquibase lock table name +|================================== + +NOTE: All the above configs can be used for multiple datasources + + +*Multiple DataSource Example:* + +If secondary dataSource named "second" is configured in application.yml +[source,yaml] +---- +include::{migrationPluginExamplesDir}/src/integration-test/resources/application-multiple-datasource.yml[lines=11..29] +---- + +The configuration for this data source would be: +[source,groovy] +---- +grails.plugin.databasemigration.reports.updateOnStart = true +grails.plugin.databasemigration.reports.changelogFileName = changelog-second.groovy +---- +The configuration for all data sources with same db schema would be: +[source,groovy] +---- +grails.plugin.databasemigration.updateAllOnStart = true +grails.plugin.databasemigration.changelogFileName = changelog.groovy +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/dbdoc.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/dbdoc.adoc new file mode 100644 index 00000000000..d15bc4dbd31 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/dbdoc.adoc @@ -0,0 +1,42 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +=== DbDoc Controller + +You can use the <> script to generate static HTML files to view changelog information, but another option is to use the `DbDocController` at runtime. By default this controller is mapped to `/appname/dbdoc/` but this can be customized with `UrlMappings` like any controller. + +You probably don't want to expose this information to all of your application's users so by default the controller is only enabled in the development environment. But you can enable or disable it for any environment in `application.groovy` with the `dbDocController.enabled` config option. For example to enable for all environments (be sure to guard the URL with a security plugin in prod): + +[source,groovy] +---- +grails.plugin.databasemigration.dbDocController.enabled = true +---- + +or to enable in the production environment: + +[source,groovy] +---- +environments { + production { + grails.plugin.databasemigration.dbDocController.enabled = true + } + ... +} +---- + diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/generalUsage.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/generalUsage.adoc new file mode 100644 index 00000000000..78c42b29f61 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/generalUsage.adoc @@ -0,0 +1,114 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +=== General Usage + +After creating the initial changelog, the typical workflow will be along the lines of: + +* make domain class changes that affect the schema +* add changes to the changelog for them +* backup your database in case something goes wrong +* run `grails dbm-update` to update your development environment (or wherever you're applying the changes) +* check the updated domain class(es) and changelog(s) into source control + +[WARNING] +===== +1. When running migration scripts on non-development databases, it's important that you backup the database before running the migration in case anything goes wrong. You could also make a copy of the database and run the script against that, and if there's a problem the real database will be unaffected. +2. Setting the dbCreate setting to "none" is recommended when executing the dbm migration commands. Otherwise you might run into troubles and the commands could not be executed. +===== + +To create the changelog additions, you can either manually create the changes or with the <> script (you can also use the <> script but it's far less convenient and requires a 2nd temporary database). + +You have a few options with `dbm-gorm-diff`: + +* `dbm-gorm-diff` will dump to the console if no filename is specified, so you can copy/paste from there +* if you include the `--add` parameter when running the script with a filename it will register an include for the filename in the main changelog for you + +Regardless of which approach you use, be sure to inspect generated changes and adjust as necessary. + + +==== Autorun on start + + +Since Liquibase maintains a record of changes that have been applied, you can avoid manually updating the database by taking advantage of the plugin's auto-run feature. By default this is disabled, but you can enable it by adding + +[source,groovy] +---- +grails.plugin.databasemigration.updateOnStart = true +---- + +to application.groovy. In addition you must specify the file containing changes; specify the name using the `updateOnStartFileName` property, e.g.: + +[source,groovy] +---- +grails.plugin.databasemigration.updateOnStartFileName = 'changelog.groovy' +---- + +Since changelogs can contain changelogs you'll most often just specify the root changelog, changelog.groovy by convention. Any changes that haven't been executed (in the specified file(s) or files included by them) will be run in the order specified. + +You may optionally limit the plugin's auto-run feature to run only specific contexts. If this configuration parameter is empty or omitted, all contexts will be run. + +[source,groovy] +---- +grails.plugin.databasemigration.updateOnStartContexts = ['context1,context2'] +---- + +You can be notified when migration are run (for example to do some work before and/or after the migrations execute) by registering a "callback" class as a Spring bean. The class can have any name and package and doesn't have to implement any interface since its methods will be called using Groovy duck-typing. + +The bean name is "migrationCallbacks" and there are currently three callback methods supported (all are optional): + +* `beforeStartMigration` will be called (if it exists) for each datasource before any migrations have run; the method will be passed a single argument, the Liquibase `Database` for that datasource +* `onStartMigration` will be called (if it exists) for each migration script; the method will be passed three arguments, the Liquibase `Database`, the `Liquibase` instance, and the changelog file name +* `afterMigrations` will be called (if it exists) for each datasource after all migrations have run; the method will be passed a single argument, the Liquibase `Database` for that datasource + +An example class will look like this: + +[source,groovy] +---- +package com.mycompany.myapp + +import liquibase.Liquibase +import liquibase.database.Database + +class MigrationCallbacks { + + void beforeStartMigration(Database Database) { + ... + } + + void onStartMigration(Database database, Liquibase liquibase, String changelogName) { + ... + } + + void afterMigrations(Database Database) { + ... + } +} +---- + +Register it in resources.groovy: + +[source,groovy] +---- +import com.mycompany.myapp.MigrationCallbacks + +beans = { + migrationCallbacks(MigrationCallbacks) +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/gettingStarted.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/gettingStarted.adoc new file mode 100644 index 00000000000..3292fa77502 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/gettingStarted.adoc @@ -0,0 +1,115 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +=== Getting Started + +*The first step is to add a dependency for the plugin in `build.gradle`:* + +[source,groovy,subs="attributes"] +---- +buildscript { + dependencies { + ... + classpath '{migrationPluginGroupId}:{migrationPluginArtifactId}:{version}' + } +} + +dependencies { + ... + implementation '{migrationPluginGroupId}:{migrationPluginArtifactId}:{version}' +} +---- + +It is also recommended to add a direct dependency to liquibase because Spring Boot overrides the one provided by this plugin + +[source,groovy,subs="attributes"] +---- +dependencies { + ... + implementation "org.liquibase:liquibase-core:{liquibaseHibernate5Version}" + implementation "org.liquibase.ext:liquibase-hibernate5:{liquibaseHibernate5Version}" +} +---- + +*Typical initial workflow* + +Next you'll need to create an initial changelog. You can use Liquibase XML or the plugin's Groovy DSL for individual files. You can even mix and match; Groovy files can include other Groovy files and Liquibase XML files (but XML files can't include Groovy files). + +Depending on the state of your database and code, you have two options; either create a changelog from the database or create it from your domain classes. The decision tends to be based on whether you prefer to design the database and adjust the domain classes to work with it, or to design your domain classes and use Hibernate to create the corresponding database structure. + +To create a changelog from the database, use the <> script: +[source,groovy] +---- +grails dbm-generate-changelog changelog.groovy +---- + +or + +[source,groovy] +---- +grails dbm-generate-changelog changelog.xml +---- + +depending on whether you prefer the Groovy DSL or XML. The filename is relative to the changelog base folder, which defaults to `grails-app/migrations`. + +NOTE: If you use the XML format (or use a non-default Groovy filename), be sure to change the name of the file in `application.groovy` so `dbm-update` and other scripts find the file: +[source,groovy] +---- +grails.plugin.databasemigration.changelogFileName = 'changelog.xml' +---- + +Since the database is already correct, run the <> script to record that the changes have already been applied: +[source,groovy] +---- +grails dbm-changelog-sync +---- + +Running this script is primarily a no-op except that it records the execution(s) in the Liquibase DATABASECHANGELOG table. + +To create a changelog from your domain classes, use the <> script: + +[source,groovy] +---- +grails dbm-generate-gorm-changelog changelog.groovy +---- + +or + +[source,groovy] +---- +grails dbm-generate-gorm-changelog changelog.xml +---- + +If you haven't created the database yet, run the <> script to create the corresponding tables: + +[source,groovy] +---- +grails dbm-update +---- + +or the <> script if the database is already in sync with your code: + +[source,groovy] +---- +grails dbm-changelog-sync +---- + +*Source control* + +Now you can commit the changelog and the corresponding application code to source control. Other developers can then update and synchronize their databases, and start doing migrations themselves. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/gorm.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/gorm.adoc new file mode 100644 index 00000000000..e299f254239 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/gorm.adoc @@ -0,0 +1,44 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +=== GORM Support + +The plugin's support for GORM is one feature that differentiates it from using Liquibase directly. Typically, when using Liquibase you make changes to a database yourself, and then create changesets manually, or use a diff script to compare your updated database to one that hasn't been updated yet. This is a decent amount of work and is rather error-prone. It's easy to forget some changes that aren't required but help performance, for example creating an index on a foreign key when using MySQL. + +*create-drop, create, and update* + +On the other end of the spectrum, Hibernate's `create-drop` mode (or `create`) will create a database that matches your domain model, but it's destructive since all previous data is lost when it runs. This works well in the very early stages of development but gets frustrating quickly. Unfortunately Hibernate's `update` mode seems like a good compromise since it only makes changes to your existing schema, but it's very limited in what it will do. It's very pessimistic and won't make any changes that could lose data. So it will add new tables and columns, but won't drop anything. If you remove a not-null domain class property you'll find you can't insert anymore since the column is still there. And it will create not-null columns as nullable since otherwise existing data would be invalid. It won't even widen a column e.g. from `VARCHAR(100)` to `VARCHAR(200)`. + +*dbm-gorm-diff* + +The plugin provides a script that will compare your GORM current domain model with a database that you specify, and the result is a Liquibase changeset - `dbm-gorm-diff`. This is the same changeset you would get if you exported your domain model to a scratch database and diffed it with the other database, but it's more convenient. + +So a good workflow would be: + +* make whatever domain class changes you need (add new ones, delete unneeded ones, add/change/remove properties, etc.) +* once your tests pass, and you're ready to commit your changes to source control, run the script to generate the changeset that will bring your database back in line with your code +* add the changeset to an existing changelog file, or use the `include` tag to include the whole file +* run the changeset on your functional test database +* assuming your functional tests pass, check everything in as one commit +* the other members of your team will get both the code and database changes when they next update, and will know to run the update script to sync their database with the latest code +* once you're ready to deploy to QA for testing (or staging or production), you can run all the un-run changes since the last deployment + +*dbm-generate-gorm-changelog* + +The <> script is useful for when you want to switch from `create-drop` mode to doing proper migrations. It's not very useful if you already have a database that's in sync with your code, since you can just use the <> script that creates a changelog from your database. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/groovyChanges.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/groovyChanges.adoc new file mode 100644 index 00000000000..63cef1ea114 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/groovyChanges.adoc @@ -0,0 +1,110 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +=== Groovy Changes + +In addition to the built-in Liquibase changes (see http://www.liquibase.org/manual/home[the documentation] for what's available) you can also make database changes using Groovy code (as long as you're using the Groovy DSL file format). These changes use the `grailsChange` tag name and are contained in a `changeSet` tag like standard built-in tags. + +There are four supported inner tags and two callable methods (to override the default confirmation message and checksum value). + +==== General format + +This is the general format of a Groovy-based change; all inner tags and methods are optional: + +[source,groovy] +---- +databaseChangeLog = { + + changeSet(author: '...', id: '...') { + + grailsChange { + init { + // arbitrary initialization code; note that no + // database or connection is available + } + + validate { + // can call warn(String message) to log a warning + // or error(String message) to stop processing + } + + change { + // arbitrary code; make changes directly and/or return a + // SqlStatement using the sqlStatement(SqlStatement sqlStatement) + // method or multiple with sqlStatements(List sqlStatements) + + confirm 'change confirmation message' + } + + rollback { + // arbitrary code; make rollback changes directly and/or + // return a SqlStatement using the sqlStatement(SqlStatement sqlStatement) + // method or multiple with sqlStatements(List sqlStatements) + + confirm 'rollback confirmation message' + } + + confirm 'confirmation message' + + checkSum 'override value for checksum' + } + + } +} +---- + +==== Available variables + +These variables are available throughout the change closure: + +* `changeSet` - the current Liquibase `ChangeSet` instance +* `resourceAccessor` - the current Liquibase `ResourceAccessor` instance +* `ctx` - the Spring `ApplicationContext` +* `application` - the `GrailsApplication` + +The `change` and `rollback` closures also have the following available: + +* `database` - the current Liquibase `Database` instance +* `databaseConnection` - the current Liquibase `DatabaseConnection` instance, which is a wrapper around the JDBC `Connection` (but doesn't implement the `Connection` interface) +* `connection` - the real JDBC `Connection` instance (a shortcut for `database.connection.wrappedConnection`) +* `sql` - a `groovy.sql.Sql` instance which uses the current `connection` and can be used for arbitrary queries and updates + +*init* + +This is where any optional initialization should happen. You can't access the database from this closure. + +*validate* + +If there are any necessary validation checks before executing changes or rollbacks they should be done here. You can log warnings by calling `warn(String message)` and stop processing by calling `error(String message)`. It may make more sense to use one or more ++preCondition++s instead of directly validating here. + +*change* + +All migration changes are done in the `change` closure. You can make changes directly (using the `sql` instance or the `connection`) and/or return one or more ++SqlStatement++s. You can call `sqlStatement(SqlStatement statement)` multiple times to register instances to be run. You can also call the `sqlStatements(statements)` method with an array or list of instances to be run. + +*rollback* + +All rollback changes are done in the `rollback` closure. You can make changes directly (using the `sql` instance or the `connection`) and/or return one or more ++SqlStatement++s. You can call `sqlStatement(SqlStatement statement)` multiple times to register instances to be run. You can also call the `sqlStatements(statements)` method with an array or list of instances to be run. + +*confirm* + +The `confirm(String message)` method is used to specify the confirmation message to be shown. The default is "Executed GrailsChange" and it can be overridden in the `change` or `rollback` closures to allow phase-specific messages or outside of both closures to use the same message for the update and rollback phase. + +*checkSum* + +The checksum for the change will be generated automatically, but if you want to override the value that gets hashed you can specify it with the `checkSum(String value)` method. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/groovyPreconditions.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/groovyPreconditions.adoc new file mode 100644 index 00000000000..b1cf087f24d --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/groovyPreconditions.adoc @@ -0,0 +1,87 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +=== Groovy Preconditions + +In addition to the built-in Liquibase preconditions (see http://www.liquibase.org/manual/preconditions[the documentation] for what's available) you can also specify preconditions using Groovy code (as long as you're using the Groovy DSL file format). These changes use the `grailsPrecondition` tag name and are contained in the `databaseChangeLog` tag or in a `changeSet` tag like standard built-in tags. + +==== General format + +This is the general format of a Groovy-based precondition: + +[source,groovy] +---- +databaseChangeLog = { + + changeSet(author: '...', id: '...') { + + preConditions { + + grailsPrecondition { + + check { + + // use an assertion + assert x == x + + // use an assertion with an error message + assert y == y : 'value cannot be 237' + + // call the fail method + if (x != x) { + fail 'x != x' + } + + // throw an exception (the fail method is preferred) + if (y != y) { + throw new RuntimeException('y != y') + } + } + + } + + } + } +} +---- + +As you can see there are a few ways to indicate that a precondition wasn't met: + +* use a simple assertion +* use an assertion with a message +* call the `fail(String message)` method (throws a `PreconditionFailedException`) +* throw an exception (shouldn't be necessary - use `assert` or `fail()` instead) + +==== Available variables + +* `database` - the current Liquibase `Database` instance +* `databaseConnection` - the current Liquibase `DatabaseConnection` instance, which is a wrapper around the JDBC `Connection` (but doesn't implement the `Connection` interface) +* `connection` - the real JDBC `Connection` instance (a shortcut for `database.connection.wrappedConnection`) +* `sql` - a `groovy.sql.Sql` instance which uses the current `connection` and can be used for arbitrary queries and updates +* `resourceAccessor` - the current Liquibase `ResourceAccessor` instance +* `ctx` - the Spring `ApplicationContext` +* `application` - the `GrailsApplication` +* `changeSet` - the current Liquibase `ChangeSet` instance +* `changeLog` - the current Liquibase `DatabaseChangeLog` instance + +==== Utility methods + +* `createDatabaseSnapshotGenerator()` - retrieves the `DatabaseSnapshotGenerator` for the current `Database` +* `createDatabaseSnapshot(String schemaName = null)` - creates a `DatabaseSnapshot` for the current `Database` (and schema if specified) + diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/index.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/index.adoc new file mode 100644 index 00000000000..a97a16969fa --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/index.adoc @@ -0,0 +1,153 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +[[introduction]] +include::introduction.adoc[] + +[[gettingStarted]] +include::gettingStarted.adoc[] + +[[configuration]] +include::configuration.adoc[] + +[[generalUsage]] +include::generalUsage.adoc[] + +[[groovyChanges]] +include::groovyChanges.adoc[] + +[[groovyPreconditions]] +include::groovyPreconditions.adoc[] + +[[gorm]] +include::gorm.adoc[] + +[[dbdoc]] +include::dbdoc.adoc[] + +[[reference]] +=== Reference + +[[ref-diff-scripts]] +==== Diff Scripts + +[[ref-diff-scripts-dbm-diff]] +include::ref/Diff Scripts/dbm-diff.adoc[] + +[[ref-diff-scripts-dbm-gorm-diff]] +include::ref/Diff Scripts/dbm-gorm-diff.adoc[] + +[[ref-documentation-scripts]] +==== Documentation Scripts + +[[ref-documentation-scripts-dbm-db-doc]] +include::ref/Documentation Scripts/dbm-db-doc.adoc[] + +[[ref-maintenance-scripts]] +==== Maintenance Scripts + +[[ref-maintenance-scripts-dbm-add-migration]] +include::ref/Maintenance Scripts/dbm-add-migration.adoc[] + +[[ref-maintenance-scripts-dbm-changelog-sync-sql]] +include::ref/Maintenance Scripts/dbm-changelog-sync-sql.adoc[] + +[[ref-maintenance-scripts-dbm-changelog-sync]] +include::ref/Maintenance Scripts/dbm-changelog-sync.adoc[] + +[[ref-maintenance-scripts-dbm-changelog-to-groovy]] +include::ref/Maintenance Scripts/dbm-changelog-to-groovy.adoc[] + +[[ref-maintenance-scripts-dbm-clear-checksums]] +include::ref/Maintenance Scripts/dbm-clear-checksums.adoc[] + +[[ref-maintenance-scripts-dbm-create-changelog]] +include::ref/Maintenance Scripts/dbm-create-changelog.adoc[] + +[[ref-maintenance-scripts-dbm-drop-all]] +include::ref/Maintenance Scripts/dbm-drop-all.adoc[] + +[[ref-maintenance-scripts-dbm-list-locks]] +include::ref/Maintenance Scripts/dbm-list-locks.adoc[] + +[[ref-maintenance-scripts-dbm-list-tags]] +include::ref/Maintenance Scripts/dbm-list-tags.adoc[] + +[[ref-maintenance-scripts-dbm-mark-next-changeset-ran]] +include::ref/Maintenance Scripts/dbm-mark-next-changeset-ran.adoc[] + +[[ref-maintenance-scripts-dbm-release-locks]] +include::ref/Maintenance Scripts/dbm-release-locks.adoc[] + +[[ref-maintenance-scripts-dbm-status]] +include::ref/Maintenance Scripts/dbm-status.adoc[] + +[[ref-maintenance-scripts-dbm-tag]] +include::ref/Maintenance Scripts/dbm-tag.adoc[] + +[[ref-maintenance-scripts-dbm-validate]] +include::ref/Maintenance Scripts/dbm-validate.adoc[] + +[[ref-rollback-scripts]] +=== Rollback Scripts + +[[ref-rollback-scripts-dbm-future-rollback-sql]] +include::ref/Rollback Scripts/dbm-future-rollback-sql.adoc[] + +[[ref-rollback-scripts-dbm-generate-changelog]] +include::ref/Rollback Scripts/dbm-generate-changelog.adoc[] + +[[ref-rollback-scripts-dbm-generate-gorm-changelog]] +include::ref/Rollback Scripts/dbm-generate-gorm-changelog.adoc[] + +[[ref-rollback-scripts-dbm-rollback-count-sql]] +include::ref/Rollback Scripts/dbm-rollback-count-sql.adoc[] + +[[ref-rollback-scripts-dbm-rollback-count]] +include::ref/Rollback Scripts/dbm-rollback-count.adoc[] + +[[ref-rollback-scripts-dbm-rollback-sql]] +include::ref/Rollback Scripts/dbm-rollback-sql.adoc[] + +[[ref-rollback-scripts-dbm-rollback-to-date-sql]] +include::ref/Rollback Scripts/dbm-rollback-to-date-sql.adoc[] + +[[ref-rollback-scripts-dbm-rollback-to-date]] +include::ref/Rollback Scripts/dbm-rollback-to-date.adoc[] + +[[ref-rollback-scripts-dbm-rollback]] +include::ref/Rollback Scripts/dbm-rollback.adoc[] + +[[ref-update-scripts]] +==== Update Scripts + +[[ref-update-scripts-dbm-previous-changeset-sql]] +include::ref/Update Scripts/dbm-previous-changeset-sql.adoc[] + +[[ref-update-scripts-dbm-update-count-sql]] +include::ref/Update Scripts/dbm-update-count-sql.adoc[] + +[[ref-update-scripts-dbm-update-count]] +include::ref/Update Scripts/dbm-update-count.adoc[] + +[[ref-update-scripts-dbm-update-sql]] +include::ref/Update Scripts/dbm-update-sql.adoc[] + +[[ref-update-scripts-dbm-update]] +include::ref/Update Scripts/dbm-update.adoc[] diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/introduction.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/introduction.adoc new file mode 100644 index 00000000000..ea707efddc8 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/introduction.adoc @@ -0,0 +1,36 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +=== Introduction + +The Database Migration plugin helps you manage database changes while developing Grails applications. The plugin uses the http://www.liquibase.org/[Liquibase] library. + +Using this plugin (and Liquibase in general) adds some structure and process to managing database changes. It will help avoid inconsistencies, communication issues, and other problems with ad-hoc approaches. + +Database migrations are represented in text form, either using a Groovy DSL or native Liquibase XML, in one or more changelog files. This approach makes it natural to maintain the changelog files in source control and also works well with branches. Changelog files can include other changelog files, so often developers create hierarchical files organized with various schemes. One popular approach is to have a root changelog named changelog.groovy (or changelog.xml) and to include a changelog per feature/branch that includes multiple smaller changelogs. Once the feature is finished and merged into the main development tree/trunk the changelog files can either stay as they are or be merged into one large file. Use whatever approach makes sense for your applications, but keep in mind that there are many options available for changelog management. + +Individual changes have an ID that should be globally unique, although they also include the username of the user making the change, making the combination of ID and username unique (although technically the ID, username, and changelog location are the "unique key"). + +As you make changes in your code (typically domain classes) that require changes in the database, you add a new change set to the changelog. Commit the code changes along with the changelog additions, and the other developers on your team will get both when they update from source control. Once they apply the new changes their code and development database will be in sync with your changes. Likewise when you deploy to a QA, a staging server, or production, you'll run the un-run changes that correspond to the code updates to being that environment's database in sync. Liquibase keeps track of previously executed changes so there's no need to think about what has and hasn't been run yet. + +*Scripts* + +Your primary interaction with the plugin will be using the provided scripts. For the most part these correspond to the many Liquibase commands that are typically executed directly from the commandline or with its Ant targets, but there are also a few Grails-specific scripts that take advantage of the information available from the GORM mappings. + +All the scripts start with `dbm-` to ensure that they're unique and don't clash with scripts from Grails or other plugins. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Diff Scripts/dbm-diff.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Diff Scripts/dbm-diff.adoc new file mode 100644 index 00000000000..9d065e97a4f --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Diff Scripts/dbm-diff.adoc @@ -0,0 +1,62 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-diff + +====== Purpose + +Compares two databases and creates a changelog that will make the changes required to bring them into sync. + +====== Description + +Executes against the database configured in `application.[yml|groovy]` for the current environment (defaults to `dev`) and another configured datasource in `application.[yml|groovy]`. + +If a filename parameter is specified then the output will be written to the named file, otherwise it will be written to the console. If the filename ends with .groovy a Groovy DSL file will be created, otherwise a standard XML file will be created. + +File are written to the migrations folder, so specify the filename relative to the migrations folder (`grails-app/migrations` by default). + +Usage: +[source,java] +---- +grails <> dbm-diff <> <> --defaultSchema=<> --dataSource=<> --add +---- + +Required arguments: + +* `otherEnv` - The name of the environment to compare to + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `defaultSchema` - The default schema name to use +* `add` - If specified add an include in the root changelog file referencing the new file +* `dataSource` - If provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `defaultSchema` and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-diff "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Diff Scripts/dbm-gorm-diff.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Diff Scripts/dbm-gorm-diff.adoc new file mode 100644 index 00000000000..f68ba1a0659 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Diff Scripts/dbm-gorm-diff.adoc @@ -0,0 +1,64 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-gorm-diff + +====== Purpose + +Diffs GORM classes against a database and generates a changelog XML or Groovy DSL file. + +====== Description + +Creates a Groovy DSL file if the filename is specified and it ends with .groovy. If another extension is specified it creates a standard Liquibase XML file, and if no filename is specified it writes to the console. + +File are written to the migrations folder, so specify the filename relative to the migrations folder (`grails-app/migrations` by default). + +Similar to <> but diffs the current configuration based on the application's domain classes with the database configured in `application.[yml|groovy]` for the current environment (defaults to `dev`). + +Doesn't modify any existing files - you need to manually merge the output into the changeset along with any necessary modifications. + +You can configure database objects to be ignored by this script - either in the GORM classes or in the target database. For example you may want domain objects that are transient, or you may have externally-managed tables, keys, etc. that you want left alone by the diff script. The configuration name for these ignored objects is `grails.plugin.databasemigration.ignoredObjects`, whose value is a list of strings. + +Usage: +[source,java] +---- +grails <> dbm-gorm-diff <> --defaultSchema=<> --dataSource=<> --add +---- + +Required arguments: _none_ . + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `defaultSchema` - The default schema name to use +* `add` - if specified add an include in the root changelog file referencing the new file +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `defaultSchema` and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-gorm-diff "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Documentation Scripts/dbm-db-doc.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Documentation Scripts/dbm-db-doc.adoc new file mode 100644 index 00000000000..70432449ff1 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Documentation Scripts/dbm-db-doc.adoc @@ -0,0 +1,55 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-db-doc + +====== Purpose + +Generates Javadoc-like documentation based on current database and change log. + +====== Description + +Writes to the folder specified by the `destination` parameter, or to the `grails.plugin.databasemigration.dbDocLocation` configuration option (defaults to `target/dbdoc`). + +Usage: +[source,java] +---- +grails <> dbm-db-doc <> --contexts=<> --dataSource=<> +---- + +Required arguments: _none_ . + +Optional arguments: + +* `destination` - The path to write to +* `contexts` - A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be included +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `contexts` and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-db-doc "--contexts=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-add-migration.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-add-migration.adoc new file mode 100644 index 00000000000..e2bf92f58a0 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-add-migration.adoc @@ -0,0 +1,41 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-add-migration + +====== Purpose + +Adds a template migration file to your project and to the changelog file. + +====== Description + +This script provides a template in which to place your migration behaviour code, whether +Grails code or raw SQL. + +Usage: +[source,java] +---- +grails <> dbm-add-migration <> +---- + +Required arguments: + +* `migrationName` - The name of the migration - will be used as a filename and the default migration id. + +NOTE: This script only supports .groovy-style migrations at the moment. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-sync-sql.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-sync-sql.adoc new file mode 100644 index 00000000000..b420bb0ea1e --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-sync-sql.adoc @@ -0,0 +1,56 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-changelog-sync-sql + +====== Purpose + +Writes the SQL that will mark all changes as executed in the database to STDOUT or a file. + +====== Description + +Generates the SQL statements for the Liquibase `DATABASECHANGELOG` control table. + +Usage: +[source,java] +---- +grails <> dbm-changelog-sync-sql <> --contexts=<> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: __none__. + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `contexts` - A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be included +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `contexts`, `defaultSchema`, and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-changelog-sync "--contexts=<>" "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]` +the suffix of `reports` will be used as the parameter value. +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-sync.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-sync.adoc new file mode 100644 index 00000000000..d01ac50af15 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-sync.adoc @@ -0,0 +1,55 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-changelog-sync + +====== Purpose + +Mark all changes as executed in the database. + +====== Description + +Registers all changesets as having been run in the Liquibase control table. This is useful when the changes have already been applied, for example if you've just created a changelog from your database using the <> script. + +Usage: +[source,java] +---- +grails <> dbm-changelog-sync --contexts=<> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: __none__. + +Optional arguments: + +* `contexts` - A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be included +* `defaultSchema` - The default schema name to use +* `dataSource` - If provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `contexts`, `defaultSchema`, and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-changelog-sync "--contexts=<>" "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-to-groovy.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-to-groovy.adoc new file mode 100644 index 00000000000..13f53ef4d99 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-changelog-to-groovy.adoc @@ -0,0 +1,42 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-changelog-to-groovy + +====== Purpose + +Converts a Liquibase XML changelog file to a Groovy DSL file. + +====== Description + +If the Groovy file name isn't specified the name and location will be the same as the original XML file with a .groovy extension. + +Usage: +[source,java] +---- +grails <> dbm-changelog-to-groovy [xml_file_name] [groovy_file_name] +---- + +Required arguments: + +* `xml_file_name` - The name and path of the XML file to convert + +Optional arguments: + +* `groovy_file_name` - The name and path of the Groovy file diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-clear-checksums.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-clear-checksums.adoc new file mode 100644 index 00000000000..06001b87fdc --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-clear-checksums.adoc @@ -0,0 +1,51 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-clear-checksums + +====== Purpose + +Removes current checksums from database. On next run checksums will be recomputed. + +====== Description + +Usage: +[source,java] +---- +grails <> dbm-clear-checksums --dataSource=<> +---- + +Required arguments: __none__. + +Optional arguments: + +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-clear-checksums "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-create-changelog.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-create-changelog.adoc new file mode 100644 index 00000000000..f31c0d4bc02 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-create-changelog.adoc @@ -0,0 +1,55 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-create-changelog + +====== Purpose + +Creates an empty changelog file. + +====== Description + +Creates a new empty file instead of generating the file from the database (using <>) or your GORM classes (using <>). + +Usage: +[source,java] +---- +grails <> dbm-create-changelog <> --dataSource=<> +---- + +Required arguments: + +* `filename` - The path to the output file to write to + +Optional arguments: + +* `dataSource` - if provided will run the script for the specified dataSource creating a file named `changelog-<>.groovy` if a `filename` is not given. Not needed for the default dataSource. + +NOTE: Note that the `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-create-changelog "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-drop-all.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-drop-all.adoc new file mode 100644 index 00000000000..558b23e220e --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-drop-all.adoc @@ -0,0 +1,53 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-drop-all + +====== Purpose + +Drops all database objects owned by the user. + +====== Description + +Usage: +[source,java] +---- +grails <> dbm-drop-all <> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: __none__. + +Optional arguments: + +* `schemaNames` - A comma-delimited list of schema names to use +* `defaultSchema` - The default schema name to use if the `schemaNames` parameter isn't present +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `defaultSchema` and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-drop-all "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-list-locks.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-list-locks.adoc new file mode 100644 index 00000000000..d1e2127cce9 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-list-locks.adoc @@ -0,0 +1,53 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-list-locks + +====== Purpose + +Lists who currently has locks on the database changelog to STDOUT or a file. + +====== Description + +Usage: +[source,java] +---- +grails <> dbm-list-locks <> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: __none__. + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `defaultSchema` and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-list-locks "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-list-tags.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-list-tags.adoc new file mode 100644 index 00000000000..f0d38873408 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-list-tags.adoc @@ -0,0 +1,46 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-list-tags + +====== Purpose + +Lists the tags in the current database. + +====== Description + +Usage: +[source,java] +---- +grails <> dbm-list-tags --defaultSchema=<> +---- + +Required arguments: + +Required arguments: __none__. + +Optional arguments: + +* `defaultSchema` - The default schema name to use + +NOTE: Note that the `defaultSchema` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-tag "--defaultSchema=<>" +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-mark-next-changeset-ran.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-mark-next-changeset-ran.adoc new file mode 100644 index 00000000000..92c9e5e9240 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-mark-next-changeset-ran.adoc @@ -0,0 +1,56 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-mark-next-changeset-ran + +====== Purpose + +Mark the next change set as executed in the database. + +====== Description + +If a filename is specified, writes the SQL that will perform the update that file but doesn't update. + +Usage: +[source,java] +---- +grails <> dbm-mark-next-changeset-ran <> --contexts=<> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: __none__. + +Optional arguments: + +* `filename` - The path to the output file to write to +* `contexts` - A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be run +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `contexts`, `defaultSchema`, and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-mark-next-changeset-ran "--contexts=<>" "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-release-locks.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-release-locks.adoc new file mode 100644 index 00000000000..1cde3f3614f --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-release-locks.adoc @@ -0,0 +1,52 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-release-locks + +====== Purpose + +Releases all locks on the database changelog. + +====== Description + +Usage: +[source,java] +---- +grails <> dbm-release-locks --defaultSchema=<> --dataSource=<> +---- + +Required arguments: __none__. + +Optional arguments: + +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `defaultSchema` and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-release-locks "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-status.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-status.adoc new file mode 100644 index 00000000000..ea161d69c21 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-status.adoc @@ -0,0 +1,55 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-status + +====== Purpose + +Outputs count or list of unrun change sets to STDOUT or a file. + +====== Description + +Usage: +[source,java] +---- +grails <> dbm-status <> --verbose=<> --contexts=<> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: __none__. + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `verbose` - If `true` (the default) the changesets are listed; if `false` only the count is displayed +* `contexts` - A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be included +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `verbose`, `contexts`, `defaultSchema` and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-status "--verbose=<>" "--contexts=<>" "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-tag.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-tag.adoc new file mode 100644 index 00000000000..01bc5759768 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-tag.adoc @@ -0,0 +1,56 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-tag + +====== Purpose + +Adds a tag to mark the current database state. + +====== Description + +Useful for future rollbacks to a specific tag (e.g. using the <> script). + +Usage: +[source,java] +---- +grails <> dbm-tag <> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: + +* `tagName` - The name of the tag to use + +Optional arguments: + +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `defaultSchema` and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-tag "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-validate.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-validate.adoc new file mode 100644 index 00000000000..69914505da5 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Maintenance Scripts/dbm-validate.adoc @@ -0,0 +1,53 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-validate + +====== Purpose + +Checks the changelog for errors. + +====== Description + +Prints any validation messages to the console. + +Usage: +[source,java] +---- +grails <> dbm-validate --dataSource=<> +---- + +Required arguments: __none__. + +Optional arguments: + +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-validate "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-future-rollback-sql.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-future-rollback-sql.adoc new file mode 100644 index 00000000000..f8cad410955 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-future-rollback-sql.adoc @@ -0,0 +1,54 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-future-rollback-sql + +====== Purpose + +Writes SQL to roll back the database to the current state after the changes in the changeslog have been applied to STDOUT or a file. + +====== Description + +Usage: +[source,java] +---- +grails <> dbm-future-rollback-sql <> --contexts=<> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: _none_ . + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `contexts` - A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be included +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `contexts`, `defaultSchema`, and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-future-rollback-sql "--contexts=<>" "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-generate-changelog.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-generate-changelog.adoc new file mode 100644 index 00000000000..fad29777c5e --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-generate-changelog.adoc @@ -0,0 +1,61 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-generate-changelog + +====== Purpose + +Generates an initial changelog XML or Groovy DSL file from the database. + +====== Description + +Creates a Groovy DSL file if the filename is specified and it ends with .groovy. If another extension is specified it creates a standard Liquibase XML file, and if no filename is specified it writes to the console. + +File are written to the migrations folder, so specify the filename relative to the migrations folder (`grails-app/migrations` by default). + +Executes against the database configured in `application.[yml|groovy]` for the current environment (defaults to `dev`). + +Usage: +[source,java] +---- +grails <> dbm-generate-changelog <> --diffTypes=<> --defaultSchema=<> --dataSource=<> --add +---- + +Required arguments: _none_ . + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `diffTypes` - A comma-delimited list of change types to include - see http://www.liquibase.org/manual/diff#controlling_checks_since_1.8[the documentation] for what types are available +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. +* `add` - if specified add an include in the root changelog file referencing the new file + +NOTE: Note that the `diffTypes`, `defaultSchema`, and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-generate-changelog "--diffTypes=<>" "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-generate-gorm-changelog.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-generate-gorm-changelog.adoc new file mode 100644 index 00000000000..cc3eba04b00 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-generate-gorm-changelog.adoc @@ -0,0 +1,59 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-generate-gorm-changelog + +====== Purpose + +Generates an initial changelog XML or Groovy DSL file from current GORM classes. + +====== Description + +Creates a Groovy DSL file if the filename is specified and it ends with .groovy. If another extension is specified it creates a standard Liquibase XML file, and if no filename is specified it writes to the console. + +File are written to the migrations folder, so specify the filename relative to the migrations folder (`grails-app/migrations` by default). + +Executes against the database configured in `DataSource.groovy` for the current environment (defaults to `dev`). + +Usage: +[source,java] +---- +grails <> dbm-generate-gorm-changelog <> --dataSource=<> --add +---- + +Required arguments: _none_ . + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. +* `add` - if specified add an include in the root changelog file referencing the new file + +NOTE: Note that the `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-generate-gorm-changelog "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-count-sql.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-count-sql.adoc new file mode 100644 index 00000000000..a43305cc85e --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-count-sql.adoc @@ -0,0 +1,57 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-rollback-count-sql + +====== Purpose + +Writes the SQL to roll back the specified number of change sets to STDOUT or a file. + +====== Description + + +Usage: +[source,java] +---- +grails <> dbm-rollback-count-sql <> <> --contexts=<> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: + +* `number` - The number of changesets to roll back + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `contexts` - A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be included +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `contexts`, `defaultSchema`, and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-rollback-count-sql "--contexts=<>" "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-count.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-count.adoc new file mode 100644 index 00000000000..2dcf1ef35b0 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-count.adoc @@ -0,0 +1,56 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-rollback-count + +====== Purpose + +Rolls back the specified number of change sets + +====== Description + + +Usage: +[source,java] +---- +grails <> dbm-rollback-count <> --contexts=<> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: + +* `number` - The number of changesets to roll back + +Optional arguments: + +* `contexts` - A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be run +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `contexts`, `defaultSchema`, and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-rollback-count "--contexts=<>" "--defaultSchema=<>" "--dataSource=<>" +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-sql.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-sql.adoc new file mode 100644 index 00000000000..80fd0e1ab7a --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-sql.adoc @@ -0,0 +1,58 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-rollback-sql + +====== Purpose + +Writes SQL to roll back the database to the state it was in when the tag was applied to STDOUT or a file. + +====== Description + +Requires that the named tag exists. You can create tags with the <> script. + +Usage: +[source,java] +---- +grails <> dbm-rollback-sql <> <> --contexts=<> --defaultSchema=<> --dataSource=<> +---- + +Required arguments: + +* `tagName` - The name of the tag to use + +Optional arguments: + +* `filename` - The path to the output file to write to. If not specified output is written to the console +* `contexts` - A comma-delimited list of http://www.liquibase.org/manual/contexts[context] names. If specified, only changesets tagged with one of the context names will be included +* `defaultSchema` - The default schema name to use +* `dataSource` - if provided will run the script for the specified dataSource. Not needed for the default dataSource. + +NOTE: Note that the `contexts`, `defaultSchema`, and `dataSource` parameter name and value must be quoted if executed in Windows, e.g. +[source,groovy] +---- +grails dbm-rollback-sql "--contexts=<>" "--defaultSchema=<>" --dataSource=<> +---- + +NOTE: For the `dataSource` parameter, if the data source is configured as `reports` underneath the `dataSources` key in `application.[yml|groovy]`, the value should be `reports`. + +[source,groovy] +---- +--dataSource=reports +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-to-date-sql.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-to-date-sql.adoc new file mode 100644 index 00000000000..ba498f2f1e0 --- /dev/null +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/databaseMigration/ref/Rollback Scripts/dbm-rollback-to-date-sql.adoc @@ -0,0 +1,60 @@ +//// +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +//// + +===== dbm-rollback-to-date-sql + +====== Purpose + +Writes SQL to roll back the database to the state it was in at the given date/time to STDOUT or a file. + +====== Description + +You can specify just the date, or the date and time. The date format must be `yyyy-MM-dd` and the time format must be `HH:mm:ss`. + +Usage: +[source,java] +---- +grails <> dbm-rollback-to-date-sql <> <