diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index a3f7a943..f2c484b4 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -949,6 +949,11 @@ component accessors="true" { arguments.value = castValueForSetter( arguments.name, arguments.value.keyValues()[ 1 ] ); } + guardAgainstLoadedKeyMutation( + arguments.name, + isNull( arguments.value ) ? javacast( "null", "" ) : arguments.value + ); + variables._data[ retrieveColumnForAlias( arguments.name ) ] = arguments.cast ? castValueForSetter( arguments.name, isNull( arguments.value ) ? javacast( "null", "" ) : arguments.value @@ -961,6 +966,28 @@ component accessors="true" { return this; } + private void function guardAgainstLoadedKeyMutation( required string name, any value ) { + if ( !isLoaded() || !arrayContainsNoCase( keyNames(), retrieveAliasForColumn( arguments.name ) ) ) { + return; + } + + var keyColumn = retrieveColumnForAlias( arguments.name ); + var originalIsNull = !variables._originalAttributes.keyExists( keyColumn ) || isNull( + variables._originalAttributes[ keyColumn ] + ); + var replacementIsNull = isNull( arguments.value ); + if ( + originalIsNull != replacementIsNull || + ( !originalIsNull && variables._originalAttributes[ keyColumn ] != arguments.value ) + ) { + throw( + type = "QuickPrimaryKeyMutationException", + message = "A loaded [#entityName()#] entity cannot change its primary key [#retrieveAliasForColumn( arguments.name )#].", + detail = "Create a new entity when a different primary key is required." + ); + } + } + /** * Retrieve an array of qualified column names. * diff --git a/models/Relationships/HasOneOrMany.cfc b/models/Relationships/HasOneOrMany.cfc index b077a942..91952307 100644 --- a/models/Relationships/HasOneOrMany.cfc +++ b/models/Relationships/HasOneOrMany.cfc @@ -303,10 +303,10 @@ component arguments.entity = arrayWrap( arguments.entity ); guardAgainstKeyLengthMismatch( arguments.entity, variables.related.keyNames() ); arguments.entity = tap( variables.related.newEntity(), function( e ) { - e.set_loaded( true ); arrayZipEach( [ variables.related.keyNames(), entity ], function( keyName, value ) { e.forceAssignAttribute( keyName, value ); } ); + e.assignOriginalAttributes( e.retrieveAttributesData() ).set_loaded( true ); } ); } setForeignAttributesForCreate( arguments.entity ); diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index 61a9d2ef..6a2f4021 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -76,6 +76,30 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( userRowsPostSave ).toHaveLength( 5 ); } ); + it( "throws a helpful error when changing the key of a loaded entity", function() { + var existingUser = getInstance( "User" ).findOrFail( 1 ); + + expect( function() { + existingUser.setId( 2 ).save(); + } ).toThrow( type = "QuickPrimaryKeyMutationException", regex = "cannot change its primary key" ); + } ); + + it( "allows assigning the existing key value to a loaded entity", function() { + var existingUser = getInstance( "User" ).findOrFail( 1 ); + + expect( function() { + existingUser.setId( 1 ).save(); + } ).notToThrow(); + } ); + + it( "guards every part of a loaded composite key", function() { + var composite = getInstance( "Composite" ).findOrFail( [ 1, 2 ] ); + + expect( function() { + composite.setB( 1 ).save(); + } ).toThrow( type = "QuickPrimaryKeyMutationException", regex = "primary key \[b\]" ); + } ); + it( "does not allow updating of column where update=false in property", function() { var existingUser = getInstance( "User" ).find( 1 ); existingUser.setEmail( "test2@test.com" );