步骤 8: 描述数据结构

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

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

    几乎所有安装好的包都会有一个配置文件放在 config/packages/ 目录下。大多数情况下,默认配置项都是精心选择的,适用于大部分应用。

    理解 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. DATABASE_URL=postgres://main::32781/main?sslmode=disable&charset=utf8
    2. # ...

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

    注解

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

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

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

    1. --- a/.env
    2. +++ b/.env
    3. @@ -24,5 +24,5 @@ APP_SECRET=ce2ae8138936039d22afb20f4596fe97
    4. #
    5. # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
    6. # DATABASE_URL="mysql://db_user::3306/db_name?serverVersion=5.7"
    7. -DATABASE_URL="postgresql://db_user:[email protected]:5432/db_name?serverVersion=13&charset=utf8"
    8. +DATABASE_URL="postgresql://127.0.0.1:5432/db?serverVersion=13&charset=utf8"
    9. ###< doctrine/doctrine-bundle ###

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

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

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

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

    1. $ symfony console make:entity Conference
    • citystring255no
    • yearstring4no
    • isInternationalbooleanno

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

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

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

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

    src/App/Entity/Conference.php

    1. namespace App\Entity;
    2. use App\Repository\ConferenceRepository;
    3. use Doctrine\ORM\Mapping as ORM;
    4. /**
    5. * @ORM\Entity(repositoryClass=ConferenceRepository::class)
    6. */
    7. class Conference
    8. {
    9. /**
    10. * @ORM\Id()
    11. * @ORM\GeneratedValue()
    12. * @ORM\Column(type="integer")
    13. */
    14. private $id;
    15. /**
    16. * @ORM\Column(type="string", length=255)
    17. */
    18. private $city;
    19. // ...
    20. public function getCity(): ?string
    21. {
    22. return $this->city;
    23. }
    24. public function setCity(string $city): self
    25. {
    26. $this->city = $city;
    27. return $this;
    28. }
    29. // ...
    30. }

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

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

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

    1. $ symfony console make:entity Comment

    输入以下回复:

    • authorstring255no
    • textno
    • emailstring255no
    • createdAtdatetimeno

    将多个实体类关联起来

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

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

    1. $ symfony console make:entity Conference
    1. Your entity already exists! So let's add some new fields!
    2. New property name (press <return> to stop adding fields):
    3. Field type (enter ? to see all types) [string]:
    4. > OneToMany
    5. What class should this entity be related to?:
    6. > Comment
    7. A new property will also be added to the Comment class...
    8. New field name inside Comment [conference]:
    9. >
    10. Is the Comment.conference property allowed to be null (nullable)? (yes/no) [yes]:
    11. > no
    12. Do you want to activate orphanRemoval on your relationship?
    13. A Comment is "orphaned" when it is removed from its related Conference.
    14. e.g. $conference->removeComment($comment)
    15. NOTE: If a Comment may *change* from one Conference to another, answer "no".
    16. Do you want to automatically delete orphaned App\Entity\Comment objects (orphanRemoval)? (yes/no) [no]:
    17. > yes
    18. updated: src/Entity/Conference.php
    19. updated: src/Entity/Comment.php

    注解

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

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

    1. --- a/src/Entity/Comment.php
    2. +++ b/src/Entity/Comment.php
    3. @@ -36,6 +36,12 @@ class Comment
    4. */
    5. private $createdAt;
    6. + /**
    7. + * @ORM\ManyToOne(targetEntity=Conference::class, inversedBy="comments")
    8. + * @ORM\JoinColumn(nullable=false)
    9. + */
    10. + private $conference;
    11. +
    12. public function getId(): ?int
    13. {
    14. return $this->id;
    15. @@ -88,4 +94,16 @@ class Comment
    16. return $this;
    17. }
    18. +
    19. + public function getConference(): ?Conference
    20. + {
    21. + return $this->conference;
    22. + }
    23. +
    24. + public function setConference(?Conference $conference): self
    25. + {
    26. + $this->conference = $conference;
    27. +
    28. + return $this;
    29. + }
    30. }
    31. --- a/src/Entity/Conference.php
    32. +++ b/src/Entity/Conference.php
    33. @@ -2,6 +2,8 @@
    34. namespace App\Entity;
    35. +use Doctrine\Common\Collections\ArrayCollection;
    36. +use Doctrine\Common\Collections\Collection;
    37. /**
    38. @@ -31,6 +33,16 @@ class Conference
    39. */
    40. private $isInternational;
    41. + /**
    42. + * @ORM\OneToMany(targetEntity=Comment::class, mappedBy="conference", orphanRemoval=true)
    43. + private $comments;
    44. +
    45. + public function __construct()
    46. + {
    47. + $this->comments = new ArrayCollection();
    48. + }
    49. +
    50. public function getId(): ?int
    51. {
    52. return $this->id;
    53. @@ -71,4 +83,35 @@ class Conference
    54. return $this;
    55. }
    56. +
    57. + /**
    58. + * @return Collection|Comment[]
    59. + */
    60. + public function getComments(): Collection
    61. + {
    62. + return $this->comments;
    63. + }
    64. +
    65. + public function addComment(Comment $comment): self
    66. + {
    67. + if (!$this->comments->contains($comment)) {
    68. + $this->comments[] = $comment;
    69. + $comment->setConference($this);
    70. + }
    71. +
    72. + return $this;
    73. + }
    74. +
    75. + public function removeComment(Comment $comment): self
    76. + {
    77. + if ($this->comments->contains($comment)) {
    78. + $this->comments->removeElement($comment);
    79. + // set the owning side to null (unless already changed)
    80. + if ($comment->getConference() === $this) {
    81. + $comment->setConference(null);
    82. + }
    83. + }
    84. +
    85. + return $this;
    86. + }
    87. }

    添加更多属性

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

    再次执行 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. namespace DoctrineMigrations;
    2. use Doctrine\DBAL\Schema\Schema;
    3. use Doctrine\Migrations\AbstractMigration;
    4. final class Version20191019083640 extends AbstractMigration
    5. {
    6. public function up(Schema $schema) : void
    7. {
    8. // this up() migration is auto-generated, please modify it to your needs
    9. $this->addSql('CREATE SEQUENCE comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
    10. $this->addSql('CREATE SEQUENCE conference_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
    11. $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))');
    12. $this->addSql('CREATE INDEX IDX_9474526C604B8382 ON comment (conference_id)');
    13. $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))');
    14. $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C604B8382 FOREIGN KEY (conference_id) REFERENCES conference (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
    15. }
    16. public function down(Schema $schema) : void
    17. {
    18. // ...
    19. }
    20. }

    更新本地数据库

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

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

    更新生产服务器

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

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

    深入学习