步骤 8: 描述数据结构

5.0 version
Maintained

描述数据结构

我们要依赖 Doctrine 来让 PHP 处理数据库,它由一组类库组成,这些类库可以帮助开发者管理数据库。

1
$ symfony composer req orm

这个命令安装了一些依赖包:Doctrine DBAL(一个数据库抽象层),Doctrine ORM(一个用 PHP 对象来管理数据库内容的库)和 Doctrine Migrations。

配置 Doctrine ORM

Doctrine 是如何知道数据库连接信息的呢?Doctrine 的 recipe 添加了 config/packages/doctrine.yaml 这个配置文件,它控制了 Doctrine 的行为方式。其中主要的设置项是 数据库的DSN,这是一个包含了所有连接信息的字符串:账号密码、服务器名、端口等。默认情况下,Doctrine 会找一个名为 DATABASE_URL 的环境变量。

理解 Symfony 的环境变量约定

你可以在 .env.env.local 文件中手工定义 DATABASE_URL 变量。事实上,你能在 .env 文件里看到 DATABASE_URL 变量的一个例子,它是由包的 recipe 所添加。但由于 Docker 暴露出来的 PostgreSQL 端口不是固定的,这个方案会很繁琐。其实有个更好的方案。

我们不用把 DATABASE_URL 硬编码在一个文件中,我们只要在所有命令前加上 symfony 前缀。这样的话 Docker 运行的服务会被自动检测到(当隧道打开的时候,SymfonyCloud 的服务也会被检测到),环境变量也会被自动设置好。

借助于环境变量,Docker Compose 以及 SymfonyCloud 可以和 Symfony 无缝对接。

通过执行 symfony var:export 来查看所有暴露出来的环境变量:

1
$ symfony var:export
1
2
DATABASE_URL=postgres://main:[email protected]:32781/main?sslmode=disable&charset=utf8
# ...

你还记得在 Docker 和 SymfonyCloud 里使用的 database 这个 服务名 吗?服务名用来作为环境变量名的前缀,比如 DATABASE_URL。如果你的服务根据 Symfony 的约定来命名,那么就不需要其它的配置了。

注解

数据库不是唯一从 Symfony 约定中受益的服务。比如,Mailer 是另外一个例子(通过 MAILER_DSN 环境变量)。

在 .env 文件中修改 DATABASE_URL 的默认值

我们仍然会修改 .env 文件来设置 DATABASE_DSN 的默认值,这样才能使用 PostgreSQL:

1
2
3
4
5
6
7
8
9
--- a/.env
+++ b/.env
@@ -25,5 +25,5 @@ APP_SECRET=447c9fa8420eb53bbd4492194b87de8f
 # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
 # For a PostgreSQL database, use: "postgresql://db_user:[email protected]:5432/db_name?serverVersion=11&charset=utf8"
 # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
-DATABASE_URL=mysql://db_user:[email protected]:3306/db_name?serverVersion=5.7
+DATABASE_URL=postgresql://127.0.0.1:5432/db?serverVersion=11&charset=utf8
 ###< doctrine/doctrine-bundle ###

为什么这些信息要在两个不同的地方重复呢?因为有些云平台上在 构建时,数据库的信息还没确定,而 Doctrine 却需要知道用哪个数据库引擎来构建它的配置。这样说来,服务器名、用户名和密码都不重要。

创建实体类

需要一些属性来描述一个会议:

  • 举行会议所在的 城市
  • 会议的 年份
  • 国际化 选项来标明这个会议是本地的还是国际的(SymfonyLive vs SymfonyCon)。

Maker Bundle 能帮我们生成一个代表会议的类(即一个 实体 类):

1
$ symfony console make:entity Conference

这个命令是交互式的:它会引导你创建所需的全部字段。在交互模式里使用以下的回复(大部分都是默认值,所以你只要按回车键就行):

  • citystring255no
  • yearstring4no
  • isInternationalbooleanno

这是执行这个命令后的全部输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
 created: src/Entity/Conference.php
 created: src/Repository/ConferenceRepository.php

 Entity generated! Now let's add some fields!
 You can always add more fields later manually or by re-running this command.

 New property name (press <return> to stop adding fields):
 > city

 Field type (enter ? to see all types) [string]:
 >

 Field length [255]:
 >

 Can this field be null in the database (nullable) (yes/no) [no]:
 >

 updated: src/Entity/Conference.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > year

 Field type (enter ? to see all types) [string]:
 >

 Field length [255]:
 > 4

 Can this field be null in the database (nullable) (yes/no) [no]:
 >

 updated: src/Entity/Conference.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > isInternational

 Field type (enter ? to see all types) [boolean]:
 >

 Can this field be null in the database (nullable) (yes/no) [no]:
 >

 updated: src/Entity/Conference.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 >



  Success!


 Next: When you're ready, create a migration with make:migration

Conference 类被放在 App\Entity\ 命名空间下。

这个命令也会生成一个 Doctrine 的 repository 类:App\Repository\ConferenceRepository

生成的代码像下面这样(只有一小部分被复制到了这):

src/App/Entity/Conference.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
namespace App\Entity;

use App\Repository\ConferenceRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=ConferenceRepository::class)
 */
class Conference
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $city;

    // ...

    public function getCity(): ?string
    {
        return $this->city;
    }

    public function setCity(string $city): self
    {
        $this->city = $city;

        return $this;
    }

    // ...
}

请注意这个类本身就是一个普通的 PHP 类,和 Doctrine 没有直接关联。Doctrine 用到的元数据是通过注解的方式添加到类里的,从而把这个类映射到相关的数据库表。

Doctrine添加了一个``id``属性来存储数据库表中的行主键。主键(@ORM\Id())的值由注解(@ORM\GeneratedValue())根据具体的数据库选用一个策略生成。

现在,我们来生成一个会议评论的实体类。

1
$ symfony console make:entity Comment

输入以下回复:

  • authorstring255no
  • texttextno
  • emailstring255no
  • createdAtdatetimeno

将多个实体类关联起来

我们要把 ConferenceComment 的实体类关联起来。一个 Conference 可以有零个或多个 Comment,这种关系被称为 一对多

再次使用 make:entity 命令,通过它把这种关系添加到 Conference 类:

1
$ symfony console make:entity Conference
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 Your entity already exists! So let's add some new fields!

 New property name (press <return> to stop adding fields):
 > comments

 Field type (enter ? to see all types) [string]:
 > OneToMany

 What class should this entity be related to?:
 > Comment

 A new property will also be added to the Comment class...

 New field name inside Comment [conference]:
 >

 Is the Comment.conference property allowed to be null (nullable)? (yes/no) [yes]:
 > no

 Do you want to activate orphanRemoval on your relationship?
 A Comment is "orphaned" when it is removed from its related Conference.
 e.g. $conference->removeComment($comment)

 NOTE: If a Comment may *change* from one Conference to another, answer "no".

 Do you want to automatically delete orphaned App\Entity\Comment objects (orphanRemoval)? (yes/no) [no]:
 > yes

 updated: src/Entity/Conference.php
 updated: src/Entity/Comment.php

注解

命令行会问你所需字段的类型,当你输入 ? 作为回复时,你能查看所有支持的类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Main types
  * string
  * text
  * boolean
  * integer (or smallint, bigint)
  * float

Relationships / Associations
  * relation (a wizard will help you build the relation)
  * ManyToOne
  * OneToMany
  * ManyToMany
  * OneToOne

Array/Object Types
  * array (or simple_array)
  * json
  * object
  * binary
  * blob

Date/Time Types
  * datetime (or datetime_immutable)
  * datetimetz (or datetimetz_immutable)
  * date (or date_immutable)
  * time (or time_immutable)
  * dateinterval

Other Types
  * decimal
  * guid
  * json_array

加好了这个关系的字段后,查看一下实体类文件的全部文件比对:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -36,6 +36,12 @@ class Comment
      */
     private $createdAt;

+    /**
+     * @ORM\ManyToOne(targetEntity=Conference::class, inversedBy="comments")
+     * @ORM\JoinColumn(nullable=false)
+     */
+    private $conference;
+
     public function getId(): ?int
     {
         return $this->id;
@@ -88,4 +94,16 @@ class Comment

         return $this;
     }
+
+    public function getConference(): ?Conference
+    {
+        return $this->conference;
+    }
+
+    public function setConference(?Conference $conference): self
+    {
+        $this->conference = $conference;
+
+        return $this;
+    }
 }
--- a/src/Entity/Conference.php
+++ b/src/Entity/Conference.php
@@ -2,6 +2,8 @@

 namespace App\Entity;

+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;

 /**
@@ -31,6 +33,16 @@ class Conference
      */
     private $isInternational;

+    /**
+     * @ORM\OneToMany(targetEntity=Comment::class, mappedBy="conference", orphanRemoval=true)
+     */
+    private $comments;
+
+    public function __construct()
+    {
+        $this->comments = new ArrayCollection();
+    }
+
     public function getId(): ?int
     {
         return $this->id;
@@ -71,4 +83,35 @@ class Conference

         return $this;
     }
+
+    /**
+     * @return Collection|Comment[]
+     */
+    public function getComments(): Collection
+    {
+        return $this->comments;
+    }
+
+    public function addComment(Comment $comment): self
+    {
+        if (!$this->comments->contains($comment)) {
+            $this->comments[] = $comment;
+            $comment->setConference($this);
+        }
+
+        return $this;
+    }
+
+    public function removeComment(Comment $comment): self
+    {
+        if ($this->comments->contains($comment)) {
+            $this->comments->removeElement($comment);
+            // set the owning side to null (unless already changed)
+            if ($comment->getConference() === $this) {
+                $comment->setConference(null);
+            }
+        }
+
+        return $this;
+    }
 }

管理类关系所需的所有代码都已经生成了。这些代码一旦生成就属于你了,你可以按照想要的方式去修改它们。

添加更多属性

我才意识到我们忘了在评论的实体类里添加一个属性:参会者可能会想要附带一张会议的照片来表达他们的反馈。

再次执行 make:entity 命令,这次增加一个 string 类型的 photoFilename 属性/列,但是要允许它可以取 null 值,因为上传照片是可选的:

1
$ symfony console make:entity Comment

迁移数据库

这两个生成的类现在完整描述了项目的数据模型。

接下去,我们需要创建与实体类对应的数据库表。

Doctrine Migrations 是完成这一任务的完美方案。它作为 orm 依赖包的一部分已经安装好了。

如果当前数据库的结构和实体类的注解定义的结构不同,就需要进行 迁移 (migration)操作。迁移 描述了当前数据库结构需要进行的更改。因为现在数据库里没有任何表,这个 迁移 会包含两个表的创建。

让我们来看下 Doctrine 生成了什么:

1
$ symfony console make:migration

请留意输出里那个生成文件的名字(一个类似 migrations/Version20191019083640.php 的名字):

migrations/Version20191019083640.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20191019083640 extends AbstractMigration
{
    public function up(Schema $schema) : void
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->addSql('CREATE SEQUENCE comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
        $this->addSql('CREATE SEQUENCE conference_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
        $this->addSql('CREATE TABLE comment (id INT NOT NULL, conference_id INT NOT NULL, author VARCHAR(255) NOT NULL, text TEXT NOT NULL, email VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, photo_filename VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
        $this->addSql('CREATE INDEX IDX_9474526C604B8382 ON comment (conference_id)');
        $this->addSql('CREATE TABLE conference (id INT NOT NULL, city VARCHAR(255) NOT NULL, year VARCHAR(4) NOT NULL, is_international BOOLEAN NOT NULL, PRIMARY KEY(id))');
        $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C604B8382 FOREIGN KEY (conference_id) REFERENCES conference (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
    }

    public function down(Schema $schema) : void
    {
        // ...
    }
}

更新本地数据库

现在你可以运行生成的迁移来更新本地数据库结构:

1
$ symfony console doctrine:migrations:migrate

现在本地数据库的结构已经是最新的了,可以准备存储数据。

更新生产服务器

迁移生产数据库结构需要的步骤和你所熟知的一样:提交代码更新后部署。

当部署项目时,SymfonyCloud 会更新代码,如果需要的话,它也会执行数据库结构迁移(它会检测 doctrine:migrations:migrate 命令是否存在)。


This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.