diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 8900ec48..c96033aa 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1037,6 +1037,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 @@ -1049,6 +1054,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 f0119d07..b878ac52 100644 --- a/models/Relationships/HasOneOrMany.cfc +++ b/models/Relationships/HasOneOrMany.cfc @@ -339,10 +339,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 a3b99ef1..476651cf 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -83,19 +83,15 @@ component extends="tests.resources.ModuleIntegrationSpec" { var originalFirstName = user.getFirstName(); var originalId = user.getId(); var dirtyFirstName = "This must not be persisted"; - var dirtyId = 9999; user.setFirstName( dirtyFirstName ); - user.setId( dirtyId ); user.touch(); expect( dateCompare( user.getCreatedDate(), originalCreated ) ).toBe( 0 ); expect( dateCompare( user.getModifiedDate(), originalModified ) ).toBe( 0 ); expect( user.getFirstName() ).toBe( dirtyFirstName ); - expect( user.getId() ).toBe( dirtyId ); expect( user.isDirty( "firstName" ) ).toBeTrue(); - expect( user.isDirty( "id" ) ).toBeTrue(); expect( user.isDirty( "createdDate" ) ).toBeFalse(); expect( user.isDirty( "modifiedDate" ) ).toBeFalse(); var freshUser = getInstance( "User" ).findOrFail( originalId ); @@ -118,6 +114,30 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( dateCompare( freshUser.getModifiedDate(), originalModified ) ).toBe( 0 ); } ); + 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" );