Skip to content

Commit 8ce7b31

Browse files
committed
document how to work with generated columns
1 parent 2c2ef65 commit 8ce7b31

File tree

6 files changed

+118
-8
lines changed

6 files changed

+118
-8
lines changed

Diff for: docs/en/cookbook/generated-columns.rst

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Generated Columns
2+
=================
3+
4+
Generated columns, sometimes also called virtual columns, are populated by
5+
the database engine itself. They are a tool for performance optimization, to
6+
avoid calculating a value on each query.
7+
8+
You can define generated columns on entities and have Doctrine map the values
9+
to your entity.
10+
11+
Declaring a generated column
12+
----------------------------
13+
14+
There is no explicit mapping instruction for generated columns. Instead, you
15+
specify that the column should not be written to, and define a custom column
16+
definition.
17+
18+
.. code-block:: php
19+
.. literalinclude:: generated-columns/Person.php
20+
21+
* ``insertable``, ``updatable``: Setting these to false tells Doctrine to never
22+
write this column - writing to a generated column would result in an error
23+
from the database.
24+
* ``columnDefinition``: We specify the full DDL to create the column. To allow
25+
to use database specific features, this attribute does not use Doctrine Query
26+
Language but native SQL. Note that you need to reference columns by their
27+
database name (either explicitly set in the mapping or per the current
28+
:doc:`naming strategy <../reference/namingstrategy>`).
29+
Be aware that specifying a column definition makes the ``SchemaTool``
30+
completely ignore all other configuration for this column. See also
31+
:ref:`#[Column] <attrref_column>`
32+
* ``generated``: Specifying that this column is always generated tells Doctrine
33+
to update the field on the entity with the value from the database after
34+
every write operation.
35+
36+
Advanced example: Extracting a value from a JSON structure
37+
----------------------------------------------------------
38+
39+
Lets assume we have an entity that stores a blogpost as structured JSON.
40+
To avoid extracting all titles on the fly when listing the posts, we create a
41+
generated column with the field.
42+
43+
.. code-block:: php
44+
.. literalinclude:: generated-columns/Article.php

Diff for: docs/en/cookbook/generated-columns/Article.php

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
#[ORM\Entity]
8+
class Article
9+
{
10+
#[ORM\Id]
11+
#[ORM\GeneratedValue]
12+
#[ORM\Column]
13+
private int $id;
14+
15+
/**
16+
* When working with Postgres, it is recommended to use the jsonb
17+
* format for better performance.
18+
*/
19+
#[ORM\Column(options: ['jsonb' => true])]
20+
private array $content;
21+
22+
/**
23+
* Because we specify NOT NULL, inserting will fail if the content does
24+
* not have a string in the title field.
25+
*/
26+
#[ORM\Column(
27+
insertable: false,
28+
updatable: false,
29+
columnDefinition: "VARCHAR(255) generated always as (content->>'title') stored NOT NULL",
30+
generated: 'ALWAYS',
31+
)]
32+
private string $title;
33+
}

Diff for: docs/en/cookbook/generated-columns/Person.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
#[ORM\Entity]
8+
class Person
9+
{
10+
#[ORM\Column(type: 'string')]
11+
private string $firstName;
12+
13+
#[ORM\Column(type: 'string', name: 'name')]
14+
private string $lastName;
15+
16+
#[ORM\Column(
17+
type: 'string',
18+
insertable: false,
19+
updatable: false,
20+
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstName, ' ', name) stored NOT NULL",
21+
generated: 'ALWAYS',
22+
)]
23+
private string $fullName;
24+
}

Diff for: docs/en/index.rst

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Cookbook
103103

104104
* **Patterns**:
105105
:doc:`Aggregate Fields <cookbook/aggregate-fields>` \|
106+
:doc:`Generated/Virtual Columns <cookbook/generated-columns>` \|
106107
:doc:`Decorator Pattern <cookbook/decorator-pattern>` \|
107108
:doc:`Strategy Pattern <cookbook/strategy-cookbook-introduction>`
108109

@@ -123,4 +124,5 @@ Cookbook
123124

124125
* **Custom Datatypes**
125126
:doc:`MySQL Enums <cookbook/mysql-enums>`
127+
:doc:`Custom Mapping Types <cookbook/custom-mapping-types>`
126128
:doc:`Advanced Field Value Conversion <cookbook/advanced-field-value-conversion-using-custom-mapping-types>`

Diff for: docs/en/reference/attributes-reference.rst

+14-8
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,15 @@ Optional parameters:
213213
- ``check``: Adds a check constraint type to the column (might not
214214
be supported by all vendors).
215215

216-
- **columnDefinition**: DDL SQL snippet that starts after the column
216+
- **columnDefinition**: Specify the DDL SQL snippet that starts after the column
217217
name and specifies the complete (non-portable!) column definition.
218218
This attribute allows to make use of advanced RMDBS features.
219-
However you should make careful use of this feature and the
220-
consequences. ``SchemaTool`` will not detect changes on the column correctly
221-
anymore if you use ``columnDefinition``.
219+
However, as this needs to be specified in the DDL native to the database,
220+
the resulting schema changes are no longer portable. If you specify a
221+
``columnDefinition``, the ``SchemaTool`` ignores all other attributes
222+
that are normally used to build the definition DDL. Changes to the
223+
``columnDefinition`` are not detected, you will need to manually create a
224+
migration to apply changes.
222225

223226
Additionally you should remember that the ``type``
224227
attribute still handles the conversion between PHP and Database
@@ -261,10 +264,11 @@ Examples:
261264
)]
262265
protected $loginCount;
263266
264-
// MySQL example: full_name char(41) GENERATED ALWAYS AS (concat(firstname,' ',lastname)),
267+
// columnDefinition is raw SQL, not DQL. This example works for MySQL:
265268
#[Column(
266269
type: "string",
267270
name: "user_fullname",
271+
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstname,' ',lastname))",
268272
insertable: false,
269273
updatable: false
270274
)]
@@ -366,7 +370,7 @@ Optional parameters:
366370

367371
- **type**: By default this is string.
368372
- **length**: By default this is 255.
369-
- **columnDefinition**: By default this is null the definition according to the type will be used. This option allows to override it.
373+
- **columnDefinition**: Allows to override how the column is generated. See the "columnDefinition" attribute on :ref:`#[Column] <attrref_column>`
370374
- **enumType**: By default this is `null`. Allows to map discriminatorColumn value to PHP enum
371375
- **options**: See "options" attribute on :ref:`#[Column] <attrref_column>`.
372376

@@ -678,8 +682,10 @@ Optional parameters:
678682
- **onDelete**: Cascade Action (Database-level)
679683
- **columnDefinition**: DDL SQL snippet that starts after the column
680684
name and specifies the complete (non-portable!) column definition.
681-
This attribute enables the use of advanced RMDBS features. Using
682-
this attribute on ``#[JoinColumn]`` is necessary if you need slightly
685+
This attribute enables the use of advanced RMDBS features. Note that you
686+
need to reference columns by their database name (either explicitly set in
687+
the mapping or per the current :doc:`naming strategy <namingstrategy>`).
688+
Using this attribute on ``#[JoinColumn]`` is necessary if you need
683689
different column definitions for joining columns, for example
684690
regarding NULL/NOT NULL defaults. However by default a
685691
"columnDefinition" attribute on :ref:`#[Column] <attrref_column>` also sets

Diff for: docs/en/sidebar.rst

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
cookbook/decorator-pattern
6666
cookbook/dql-custom-walkers
6767
cookbook/dql-user-defined-functions
68+
cookbook/generated-columns
6869
cookbook/implementing-arrayaccess-for-domain-objects
6970
cookbook/implementing-the-notify-changetracking-policy
7071
cookbook/resolve-target-entity-listener

0 commit comments

Comments
 (0)