步骤 8: 描述数据结构

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

  1. $ symfony composer req "orm:^2"

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

配置 Doctrine ORM

Doctrine 是如何知道数据库连接信息的呢?Doctrine 的 recipe 添加了 config/packages/doctrine.yaml 这个配置文件,它控制了 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:[email protected]: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:[email protected]: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

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

  1. created: src/Entity/Conference.php
  2. created: src/Repository/ConferenceRepository.php
  3. Entity generated! Now let's add some fields!
  4. You can always add more fields later manually or by re-running this command.
  5. New property name (press <return> to stop adding fields):
  6. > city
  7. Field type (enter ? to see all types) [string]:
  8. >
  9. Field length [255]:
  10. >
  11. Can this field be null in the database (nullable) (yes/no) [no]:
  12. >
  13. updated: src/Entity/Conference.php
  14. Add another property? Enter the property name (or press <return> to stop adding fields):
  15. > year
  16. Field type (enter ? to see all types) [string]:
  17. >
  18. Field length [255]:
  19. > 4
  20. Can this field be null in the database (nullable) (yes/no) [no]:
  21. >
  22. updated: src/Entity/Conference.php
  23. Add another property? Enter the property name (or press <return> to stop adding fields):
  24. > isInternational
  25. Field type (enter ? to see all types) [boolean]:
  26. >
  27. Can this field be null in the database (nullable) (yes/no) [no]:
  28. >
  29. updated: src/Entity/Conference.php
  30. Add another property? Enter the property name (or press <return> to stop adding fields):
  31. >
  32. Success!
  33. 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. 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
  • texttextno
  • 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. > comments
  4. Field type (enter ? to see all types) [string]:
  5. > OneToMany
  6. What class should this entity be related to?:
  7. > Comment
  8. A new property will also be added to the Comment class...
  9. New field name inside Comment [conference]:
  10. >
  11. Is the Comment.conference property allowed to be null (nullable)? (yes/no) [yes]:
  12. > no
  13. Do you want to activate orphanRemoval on your relationship?
  14. A Comment is "orphaned" when it is removed from its related Conference.
  15. e.g. $conference->removeComment($comment)
  16. NOTE: If a Comment may *change* from one Conference to another, answer "no".
  17. Do you want to automatically delete orphaned App\Entity\Comment objects (orphanRemoval)? (yes/no) [no]:
  18. > yes
  19. updated: src/Entity/Conference.php
  20. updated: src/Entity/Comment.php

注解

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

  1. Main types
  2. * string
  3. * text
  4. * boolean
  5. * integer (or smallint, bigint)
  6. * float
  7. Relationships / Associations
  8. * relation (a wizard will help you build the relation)
  9. * ManyToOne
  10. * OneToMany
  11. * ManyToMany
  12. * OneToOne
  13. Array/Object Types
  14. * array (or simple_array)
  15. * json
  16. * object
  17. * binary
  18. * blob
  19. Date/Time Types
  20. * datetime (or datetime_immutable)
  21. * datetimetz (or datetimetz_immutable)
  22. * date (or date_immutable)
  23. * time (or time_immutable)
  24. * dateinterval
  25. Other Types
  26. * decimal
  27. * guid
  28. * json_array

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

  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. use Doctrine\ORM\Mapping as ORM;
  38. /**
  39. @@ -31,6 +33,16 @@ class Conference
  40. */
  41. private $isInternational;
  42. + /**
  43. + * @ORM\OneToMany(targetEntity=Comment::class, mappedBy="conference", orphanRemoval=true)
  44. + */
  45. + private $comments;
  46. +
  47. + public function __construct()
  48. + {
  49. + $this->comments = new ArrayCollection();
  50. + }
  51. +
  52. public function getId(): ?int
  53. {
  54. return $this->id;
  55. @@ -71,4 +83,35 @@ class Conference
  56. return $this;
  57. }
  58. +
  59. + /**
  60. + * @return Collection|Comment[]
  61. + */
  62. + public function getComments(): Collection
  63. + {
  64. + return $this->comments;
  65. + }
  66. +
  67. + public function addComment(Comment $comment): self
  68. + {
  69. + if (!$this->comments->contains($comment)) {
  70. + $this->comments[] = $comment;
  71. + $comment->setConference($this);
  72. + }
  73. +
  74. + return $this;
  75. + }
  76. +
  77. + public function removeComment(Comment $comment): self
  78. + {
  79. + if ($this->comments->contains($comment)) {
  80. + $this->comments->removeElement($comment);
  81. + // set the owning side to null (unless already changed)
  82. + if ($comment->getConference() === $this) {
  83. + $comment->setConference(null);
  84. + }
  85. + }
  86. +
  87. + return $this;
  88. + }
  89. }

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

添加更多属性

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

再次执行 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. }

更新本地数据库

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

  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.