Skip to content

Commit e369cb6

Browse files
mehldaubeberleigreg0ire
authored
Generated/Virtual Columns: Insertable / Updateable (#9118)
* Generated/Virtual Columns: Insertable / Updateable Defines whether a column is included in an SQL INSERT and/or UPDATE statement. Throws an exception for UPDATE statements attempting to update this field/column. Closes #5728 * Apply suggestions from code review Co-authored-by: Grégoire Paris <[email protected]> * Add example for virtual column usage in attributes to docs. Co-authored-by: Benjamin Eberlei <[email protected]> Co-authored-by: Grégoire Paris <[email protected]>
1 parent ec391be commit e369cb6

29 files changed

+737
-60
lines changed

docs/en/reference/annotations-reference.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ Optional attributes:
123123

124124
- **nullable**: Determines if NULL values allowed for this column. If not specified, default value is false.
125125

126+
- **insertable**: Boolean value to determine if the column should be
127+
included when inserting a new row into the underlying entities table.
128+
If not specified, default value is true.
129+
130+
- **updatable**: Boolean value to determine if the column should be
131+
included when updating the row of the underlying entities table.
132+
If not specified, default value is true.
133+
134+
- **generated**: An enum with the possible values ALWAYS, INSERT, NEVER. Is
135+
used after an INSERT or UPDATE statement to determine if the database
136+
generated this value and it needs to be fetched using a SELECT statement.
137+
126138
- **options**: Array of additional options:
127139

128140
- ``default``: The default value to set for the column if no value
@@ -193,6 +205,13 @@ Examples:
193205
*/
194206
protected $loginCount;
195207
208+
/**
209+
* Generated column
210+
* @Column(type="string", name="user_fullname", insertable=false, updatable=false)
211+
* MySQL example: full_name char(41) GENERATED ALWAYS AS (concat(firstname,' ',lastname)),
212+
*/
213+
protected $fullname;
214+
196215
.. _annref_column_result:
197216

198217
@ColumnResult

docs/en/reference/attributes-reference.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,18 @@ Optional parameters:
178178
- **nullable**: Determines if NULL values allowed for this column.
179179
If not specified, default value is ``false``.
180180

181+
- **insertable**: Boolean value to determine if the column should be
182+
included when inserting a new row into the underlying entities table.
183+
If not specified, default value is true.
184+
185+
- **updatable**: Boolean value to determine if the column should be
186+
included when updating the row of the underlying entities table.
187+
If not specified, default value is true.
188+
189+
- **generated**: An enum with the possible values ALWAYS, INSERT, NEVER. Is
190+
used after an INSERT or UPDATE statement to determine if the database
191+
generated this value and it needs to be fetched using a SELECT statement.
192+
181193
- **options**: Array of additional options:
182194

183195
- ``default``: The default value to set for the column if no value
@@ -248,6 +260,15 @@ Examples:
248260
)]
249261
protected $loginCount;
250262
263+
// MySQL example: full_name char(41) GENERATED ALWAYS AS (concat(firstname,' ',lastname)),
264+
#[Column(
265+
type: "string",
266+
name: "user_fullname",
267+
insertable: false,
268+
updatable: false
269+
)]
270+
protected $fullname;
271+
251272
.. _attrref_cache:
252273

253274
#[Cache]

docs/en/reference/basic-mapping.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ list:
199199
unique key.
200200
- ``nullable``: (optional, default FALSE) Whether the database
201201
column is nullable.
202+
- ``insertable``: (optional, default TRUE) Whether the database
203+
column should be inserted.
204+
- ``updatable``: (optional, default TRUE) Whether the database
205+
column should be updated.
202206
- ``enumType``: (optional, requires PHP 8.1 and ORM 2.11) The PHP enum type
203207
name to convert the database value into.
204208
- ``precision``: (optional, default 0) The precision for a decimal

docs/en/reference/xml-mapping.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,11 @@ Optional attributes:
256256
table? Defaults to false.
257257
- nullable - Should this field allow NULL as a value? Defaults to
258258
false.
259+
- insertable - Should this field be inserted? Defaults to true.
260+
- updatable - Should this field be updated? Defaults to true.
261+
- generated - Enum of the values ALWAYS, INSERT, NEVER that determines if
262+
generated value must be fetched from database after INSERT or UPDATE.
263+
Defaults to "NEVER".
259264
- version - Should this field be used for optimistic locking? Only
260265
works on fields with type integer or datetime.
261266
- scale - Scale of a decimal type.

doctrine-mapping.xsd

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@
288288
</xs:restriction>
289289
</xs:simpleType>
290290

291+
<xs:simpleType name="generated-type">
292+
<xs:restriction base="xs:token">
293+
<xs:enumeration value="NEVER"/>
294+
<xs:enumeration value="INSERT"/>
295+
<xs:enumeration value="ALWAYS"/>
296+
</xs:restriction>
297+
</xs:simpleType>
298+
291299
<xs:complexType name="field">
292300
<xs:choice minOccurs="0" maxOccurs="unbounded">
293301
<xs:element name="options" type="orm:options" minOccurs="0" />
@@ -299,6 +307,9 @@
299307
<xs:attribute name="length" type="xs:NMTOKEN" />
300308
<xs:attribute name="unique" type="xs:boolean" default="false" />
301309
<xs:attribute name="nullable" type="xs:boolean" default="false" />
310+
<xs:attribute name="insertable" type="xs:boolean" default="true" />
311+
<xs:attribute name="updatable" type="xs:boolean" default="true" />
312+
<xs:attribute name="generated" type="orm:generated-type" default="NEVER" />
302313
<xs:attribute name="enum-type" type="xs:string" />
303314
<xs:attribute name="version" type="xs:boolean" />
304315
<xs:attribute name="column-definition" type="xs:string" />
@@ -623,6 +634,8 @@
623634
<xs:attribute name="length" type="xs:NMTOKEN" />
624635
<xs:attribute name="unique" type="xs:boolean" default="false" />
625636
<xs:attribute name="nullable" type="xs:boolean" default="false" />
637+
<xs:attribute name="insertable" type="xs:boolean" default="true" />
638+
<xs:attribute name="updateable" type="xs:boolean" default="true" />
626639
<xs:attribute name="version" type="xs:boolean" />
627640
<xs:attribute name="column-definition" type="xs:string" />
628641
<xs:attribute name="precision" type="xs:integer" use="optional" />

lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,16 @@ public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $e
5555
$data = $this->uow->getOriginalEntityData($entity);
5656
$data = array_merge($data, $metadata->getIdentifierValues($entity)); // why update has no identifier values ?
5757
58-
if ($metadata->isVersioned) {
59-
$data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField);
58+
if ($metadata->requiresFetchAfterChange) {
59+
if ($metadata->isVersioned) {
60+
$data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField);
61+
}
62+
63+
foreach ($metadata->fieldMappings as $name => $fieldMapping) {
64+
if (isset($fieldMapping['generated'])) {
65+
$data[$name] = $metadata->getFieldValue($entity, $name);
66+
}
67+
}
6068
}
6169

6270
foreach ($metadata->associationMappings as $name => $assoc) {

lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,34 @@ public function precision($p)
110110
return $this;
111111
}
112112

113+
/**
114+
* Sets insertable.
115+
*
116+
* @return $this
117+
*/
118+
public function insertable(bool $flag = true): self
119+
{
120+
if (! $flag) {
121+
$this->mapping['notInsertable'] = true;
122+
}
123+
124+
return $this;
125+
}
126+
127+
/**
128+
* Sets updatable.
129+
*
130+
* @return $this
131+
*/
132+
public function updatable(bool $flag = true): self
133+
{
134+
if (! $flag) {
135+
$this->mapping['notUpdatable'] = true;
136+
}
137+
138+
return $this;
139+
}
140+
113141
/**
114142
* Sets scale.
115143
*

lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@
8080
* length?: int,
8181
* id?: bool,
8282
* nullable?: bool,
83+
* notInsertable?: bool,
84+
* notUpdatable?: bool,
85+
* generated?: string,
8386
* enumType?: class-string<BackedEnum>,
8487
* columnDefinition?: string,
8588
* precision?: int,
@@ -258,6 +261,21 @@ class ClassMetadataInfo implements ClassMetadata
258261
*/
259262
public const CACHE_USAGE_READ_WRITE = 3;
260263

264+
/**
265+
* The value of this column is never generated by the database.
266+
*/
267+
public const GENERATED_NEVER = 0;
268+
269+
/**
270+
* The value of this column is generated by the database on INSERT, but not on UPDATE.
271+
*/
272+
public const GENERATED_INSERT = 1;
273+
274+
/**
275+
* The value of this column is generated by the database on both INSERT and UDPATE statements.
276+
*/
277+
public const GENERATED_ALWAYS = 2;
278+
261279
/**
262280
* READ-ONLY: The name of the entity class.
263281
*
@@ -439,6 +457,12 @@ class ClassMetadataInfo implements ClassMetadata
439457
* - <b>nullable</b> (boolean, optional)
440458
* Whether the column is nullable. Defaults to FALSE.
441459
*
460+
* - <b>'notInsertable'</b> (boolean, optional)
461+
* Whether the column is not insertable. Optional. Is only set if value is TRUE.
462+
*
463+
* - <b>'notUpdatable'</b> (boolean, optional)
464+
* Whether the column is updatable. Optional. Is only set if value is TRUE.
465+
*
442466
* - <b>columnDefinition</b> (string, optional, schema-only)
443467
* The SQL fragment that is used when generating the DDL for the column.
444468
*
@@ -659,13 +683,21 @@ class ClassMetadataInfo implements ClassMetadata
659683
*/
660684
public $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
661685

686+
/**
687+
* READ-ONLY: A Flag indicating whether one or more columns of this class
688+
* have to be reloaded after insert / update operations.
689+
*
690+
* @var bool
691+
*/
692+
public $requiresFetchAfterChange = false;
693+
662694
/**
663695
* READ-ONLY: A flag for whether or not instances of this class are to be versioned
664696
* with optimistic locking.
665697
*
666698
* @var bool
667699
*/
668-
public $isVersioned;
700+
public $isVersioned = false;
669701

670702
/**
671703
* READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
@@ -963,6 +995,10 @@ public function __sleep()
963995
$serialized[] = 'cache';
964996
}
965997

998+
if ($this->requiresFetchAfterChange) {
999+
$serialized[] = 'requiresFetchAfterChange';
1000+
}
1001+
9661002
return $serialized;
9671003
}
9681004

@@ -1611,6 +1647,16 @@ protected function validateAndCompleteFieldMapping(array $mapping): array
16111647
$mapping['requireSQLConversion'] = true;
16121648
}
16131649

1650+
if (isset($mapping['generated'])) {
1651+
if (! in_array($mapping['generated'], [self::GENERATED_NEVER, self::GENERATED_INSERT, self::GENERATED_ALWAYS])) {
1652+
throw MappingException::invalidGeneratedMode($mapping['generated']);
1653+
}
1654+
1655+
if ($mapping['generated'] === self::GENERATED_NEVER) {
1656+
unset($mapping['generated']);
1657+
}
1658+
}
1659+
16141660
if (isset($mapping['enumType'])) {
16151661
if (PHP_VERSION_ID < 80100) {
16161662
throw MappingException::enumsRequirePhp81($this->name, $mapping['fieldName']);
@@ -2675,6 +2721,10 @@ public function mapField(array $mapping)
26752721
$mapping = $this->validateAndCompleteFieldMapping($mapping);
26762722
$this->assertFieldNotMapped($mapping['fieldName']);
26772723

2724+
if (isset($mapping['generated'])) {
2725+
$this->requiresFetchAfterChange = true;
2726+
}
2727+
26782728
$this->fieldMappings[$mapping['fieldName']] = $mapping;
26792729
}
26802730

@@ -3405,8 +3455,9 @@ public function setSequenceGeneratorDefinition(array $definition)
34053455
*/
34063456
public function setVersionMapping(array &$mapping)
34073457
{
3408-
$this->isVersioned = true;
3409-
$this->versionField = $mapping['fieldName'];
3458+
$this->isVersioned = true;
3459+
$this->versionField = $mapping['fieldName'];
3460+
$this->requiresFetchAfterChange = true;
34103461

34113462
if (! isset($mapping['default'])) {
34123463
if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) {
@@ -3429,6 +3480,10 @@ public function setVersionMapping(array &$mapping)
34293480
public function setVersioned($bool)
34303481
{
34313482
$this->isVersioned = $bool;
3483+
3484+
if ($bool) {
3485+
$this->requiresFetchAfterChange = true;
3486+
}
34323487
}
34333488

34343489
/**

lib/Doctrine/ORM/Mapping/Column.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ final class Column implements Annotation
4444
/** @var bool */
4545
public $nullable = false;
4646

47+
/** @var bool */
48+
public $insertable = true;
49+
50+
/** @var bool */
51+
public $updatable = true;
52+
4753
/** @var class-string<\BackedEnum>|null */
4854
public $enumType = null;
4955

@@ -53,9 +59,17 @@ final class Column implements Annotation
5359
/** @var string|null */
5460
public $columnDefinition;
5561

62+
/**
63+
* @var string|null
64+
* @psalm-var 'NEVER'|'INSERT'|'ALWAYS'|null
65+
* @Enum({"NEVER", "INSERT", "ALWAYS"})
66+
*/
67+
public $generated;
68+
5669
/**
5770
* @param class-string<\BackedEnum>|null $enumType
5871
* @param array<string,mixed> $options
72+
* @psalm-param 'NEVER'|'INSERT'|'ALWAYS'|null $generated
5973
*/
6074
public function __construct(
6175
?string $name = null,
@@ -65,9 +79,12 @@ public function __construct(
6579
?int $scale = null,
6680
bool $unique = false,
6781
bool $nullable = false,
82+
bool $insertable = true,
83+
bool $updatable = true,
6884
?string $enumType = null,
6985
array $options = [],
70-
?string $columnDefinition = null
86+
?string $columnDefinition = null,
87+
?string $generated = null
7188
) {
7289
$this->name = $name;
7390
$this->type = $type;
@@ -76,8 +93,11 @@ public function __construct(
7693
$this->scale = $scale;
7794
$this->unique = $unique;
7895
$this->nullable = $nullable;
96+
$this->insertable = $insertable;
97+
$this->updatable = $updatable;
7998
$this->enumType = $enumType;
8099
$this->options = $options;
81100
$this->columnDefinition = $columnDefinition;
101+
$this->generated = $generated;
82102
}
83103
}

0 commit comments

Comments
 (0)