Etap 8: Opis struktury danych
Opis struktury danych¶
Do komunikacji PHP z bazą danych użyjemy Doctrine. Jest to zestaw bibliotek, które pomagają zarządzać bazami danych:
1 | $ symfony composer req "orm:^2"
|
To polecenie instaluje kilka zależności: Doctrine DBAL (warstwa abstrakcji bazy danych), Doctrine ORM (biblioteka do modyfikacji zawartości bazy danych przy użyciu obiektów PHP) oraz Doctrine Migrations (narzędzia ułatwiające zmianę struktury bazy danych).
Konfigurowanie Doctrine ORM¶
Skąd Doctrine wie, jak połączyć się z bazą danych? Przepis (ang. recipe) instalujący Doctrine dodał odpowiedni plik konfiguracyjny (config/packages/doctrine.yaml
), który kontroluje jego zachowanie. Głównym ustawieniem jest database DSN, napis zawierający wszystkie informacje o połączeniu: dane uwierzytelniające, host, port itp. Domyślnie Doctrine szuka zmiennej środowiskowej DATABASE_URL
.
Prawie wszystkie zainstalowane pakiety posiadają swoją konfigurację w katalogu config/packages/
. W większości przypadków wartości domyślne były wybierane ostrożnie, aby działały w większości aplikacji.
Zrozumienie konwencji zmiennych środowiskowych w Symfony¶
Możesz zdefiniować zmienną środowiskową DATABASE_URL
ręcznie w pliku .env
lub .env.local
. Dzięki przepisowi (ang. recipe) pakietu, możesz bazować na przykładowej wartości zmiennej środowiskowej DATABASE_URL
już wpisanej w pliku .env
. Pojawia się jednak problem uciążliwej ręcznej aktualizacji wpisu po każdej zmianie portu bazy danych PostgreSQL udostępnionego przez Dockera. Lepiej więc podejść do sprawy w inny sposób.
Zamiast dokonywać sztywnego ustawienia zmiennej środowiskowej DATABASE_URL
w pliku, możesz poprzedzać wszystkie polecenia słowem symfony
. Dzięki temu wszystkie usługi działające w kontenerze Docker i/lub SymfonyCloud (wyłącznie jeśli mamy otwarty tunel z SymfonyCloud) będą automatycznie ustawione jako zmienne środowiskowe.
Dzięki zmiennym środowiskowym integracja Symfony z Docker Compose i SymfonyCloud jest bezproblemowa.
Możesz sprawdzić aktualne zmienne środowiskowe w konsoli poprzez użycie polecenia symfony var:export
:
1 | $ symfony var:export
|
1 2 | DATABASE_URL=postgres://main:[email protected]:32781/main?sslmode=disable&charset=utf8
# ...
|
Pamiętasz nazwę usługi database
, której użyliśmy w konfiguracji Docker i SymfonyCloud? Nazwy usług są używane jako prefiksy do definiowania zmiennych środowiskowych, takich jak DATABASE_URL
. Jeśli twoje usługi są nazwane zgodnie z konwencjami Symfony, żadna dodatkowa konfiguracja nie jest potrzebna.
Informacja
Bazy danych nie są jedyną usługą, która korzysta tej z konwencji. To samo dotyczy na przykład Mailera (zmienna środowiskowa MAILER_DSN
).
Zmiana domyślnej wartości DATABASE_URL w pliku .env¶
Zmienimy plik .env
tak, aby ustawić domyślną wartość zmiennej środowiskowej DATABASE_URL
dla PostgreSQL:
1 2 3 4 5 6 7 8 9 | --- a/.env
+++ b/.env
@@ -24,5 +24,5 @@ APP_SECRET=ce2ae8138936039d22afb20f4596fe97
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://db_user:[email protected]:3306/db_name?serverVersion=5.7"
-DATABASE_URL="postgresql://db_user:[email protected]:5432/db_name?serverVersion=13&charset=utf8"
+DATABASE_URL="postgresql://127.0.0.1:5432/db?serverVersion=13&charset=utf8"
###< doctrine/doctrine-bundle ###
|
Dlaczego musimy powielać tę samą informację w dwóch różnych miejscach? Ponieważ na niektórych platformach chmurowych, w czasie budowania aplikacji, adres URL bazy danych może nie być jeszcze znany, a Doctrine potrzebuje informacji o silniku bazy danych, aby zbudować odpowiednią dla niego konfigurację. Tak więc host, nazwa użytkownika i hasło nie mają większego znaczenia.
Tworzenie klas encji¶
Konferencję można opisać kilkoma atrybutami:
- city - miasto, w którym organizowana jest konferencja;
- year - rok, w którym odbywa się konferencja;
- isInternational - flaga wskazująca, czy konferencja jest krajowa, czy międzynarodowa (SymfonyLive vs SymfonyCon).
Maker Bundle pomoże nam wygenerować klasę encji (ang. entity), która będzie reprezentowała konferencję:
1 | $ symfony console make:entity Conference
|
To polecenie jest uruchamiane w trybie interaktywnym - poprowadzi Cię przez proces dodawania wszystkich potrzebnych pól. Użyj następujących odpowiedzi (większość z nich to odpowiedzi domyślne, więc możesz nacisnąć klawisz „Enter”, aby ich użyć):
city
,string
,255
,no
;year
,string
,4
,no
;isInternational
,boolean
,no
.
Oto pełne wyjście (ang. output) po uruchomieniu polecenia:
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
|
Klasa Conference
została zapisana w przestrzeni nazw App\Entity\
.
Polecenie wygenerowało również klasę repozytorium (ang. repository) Doctrine: App\Repository\ConferenceRepository
.
Wygenerowany kod wygląda następująco (tylko niewielka część pliku jest tu pokazana):
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;
}
// ...
}
|
Zauważ, że właśnie utworzona klasa jest zwykłą klasą PHP - nie ma w niej elementów Doctrine. Metadane wykorzystywane przez Doctrine do powiązania klasy z tabelą w bazie danych dodajemy, używając adnotacji.
Doctrine dodał atrybut id
aby zachować klucz główny w tabeli bazy danych. Ten klucz (@ORM\Id()
) jest automatycznie generowany (@ORM\GeneratedValue()
) w sposób zależny od silnika bazy danych (oparty o wzorzec strategii).
Teraz wygeneruj klasę encji dla komentarzy:
Wprowadź następujące odpowiedzi:
author
,string
,255
,no
;text
,text
,no
;email
,string
,255
,no
;createdAt
,datetime
,no
.
Łączenie encji¶
Obie encje, Conference
i Comment
, powinny być ze sobą powiązane. Konferencja może mieć zero lub więcej komentarzy, co nazywamy relacją jeden do wielu (ang. one to many).
Użyj ponownie polecenia make:entity
, aby zdefiniować tę relację w klasie 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
|
Informacja
Jeśli wpiszesz ?
jako odpowiedź w pytaniu o typ pola, otrzymasz listę wszystkich obsługiwanych typów:
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
|
Przyjrzyj się liście różnic dla klas encji po dodaniu relacji:
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;
+ }
}
|
Wszystko, czego potrzebujesz do zarządzania tą relacją, zostało wygenerowane automatycznie. Później kod możesz zmieniać jak chcesz.
Dodawanie kolejnych atrybutów¶
Właśnie zdałem sobie sprawę, że zapomnieliśmy dodać pewien atrybut w encji Comment
: uczestnicy mogą chcieć dołączyć zdjęcie z konferencji, aby zilustrować swoje opinie.
Uruchom make:entity
jeszcze raz i dodaj atrybut photoFilename
jako kolumnę typu string
. Pozwól jej przyjmować wartość null
, ponieważ dodanie zdjęcia jest opcjonalne:
1 | $ symfony console make:entity Comment
|
Migracja bazy danych¶
Model projektu składa się teraz z dwóch właśnie wygenerowanych klas.
W kolejnym kroku musimy utworzyć tabele w bazie danych związane z naszymi encjami w PHP.
Biblioteka Doctrine Migrations to narzędzie idealnie dopasowane do tego zadania. Została ona już zainstalowana jako część zależności orm
.
Migracja (ang. migration) jest klasą, która opisuje zmiany wykonywane w bazie danych, aby z obecnego schematu przejść na nowy, zdefiniowany w adnotacjach encji. Ponieważ baza danych jest na razie pusta, migracja powinna składać się z operacji tworzących dwie tabele.
Zobaczmy, co wygeneruje Doctrine:
1 | $ symfony console make:migration
|
Zwróć uwagę na wygenerowaną nazwę pliku, która powinna przypominać 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
{
// ...
}
}
|
Aktualizacja lokalnej bazy danych¶
Możesz teraz uruchomić migrację, aby zaktualizować schemat lokalnej bazy danych:
1 | $ symfony console doctrine:migrations:migrate
|
Schemat lokalnej bazy danych jest teraz aktualny i przygotowany do przechowywania niektórych danych.
Aktualizacja produkcyjnej bazy danych¶
Kroki potrzebne do wykonania migracji na produkcyjnej bazie danych są takie same jak te, które już znasz: zatwierdź zmiany (ang. commit) i wdrażaj.
Podczas wdrażania projektu, SymfonyCloud oprócz aktualizacji kodu uruchamia także migrację bazy danych, jeśli taka istnieje (wykrywa, czy istnieje polecenie doctrine:migrations:migrate
).
- « Previous Etap 7: Konfigurowanie bazy danych
- Next » Etap 9: Konfigurowanie panelu administracyjnego
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.