owned this note
owned this note
Published
Linked with GitHub
# Database 與 Doctrine ORM
Symfony 提供了在應用程式中使用資料庫所需的所有工具,這要歸功於Doctrine,這是處理資料庫的最佳 PHP libraires。
這些工具支持 MySQL 和 PostgreSQL 等關聯性資料庫以及NoSQL資料庫如MongoDB 。
- 此文件解釋了在 Symfony 應用程式中使用關聯資料庫的推薦方法;
- 如果需要低層級訪問權限來對關聯資料庫執行原始的SQL \查詢(類似於 PHP 的PDO),請閱讀[這裡](https://symfony.com/doc/4.4/doctrine/dbal.html)
- 如果你使用MongoDB 資料庫,請閱讀[DoctrineMongoDBBundle](https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html)文件。
## 安裝 Doctrine
透過```orm```[symfony pack](https://symfony.com/doc/4.4/setup.html#symfony-packs)安裝Doctrine 的相關支援,這將有助於生成一些code:
```
composer require symfony/orm-pack
composer require --dev symfony/maker-bundle
```
## 配置資料庫
資料庫連接資訊存儲為名為```DATABASE+URL```的環境變量。
對於開發環境,你可以在下面找到並客製化這個```.env```:
```
# .env (or override DATABASE_URL in .env.local to avoid committing your changes)
# customize this line!
DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
# to use mariadb:
DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=mariadb-10.5.8"
# to use sqlite:
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"
# to use postgresql:
# DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
# to use oracle:
# DATABASE_URL="oci8://db_user:db_password@127.0.0.1:1521/db_name"
```
:::danger
Caution
如果用戶名稱、密碼、host或資料庫名稱在 URI 中包含任何被視為特殊的符號(例如+, @, $, #, /, :, *, !),必須對它們進行encode。
有關保留關鍵字的完整列表,請參閱[RFC 3986](https://www.ietf.org/rfc/rfc3986.txt),或使用```urlencode```函數對其進行encode。
在這種情況下,需要刪除```resolve:```前綴``` inconfig/packages/doctrine.yaml```以避免錯誤:``` url:'%env(resolve:DATABASE_URL)%'```
:::
現在連接參數已設置,Doctrine可以透過```db_name```創建你的資料庫:
```
php bin/console doctrine:database:create
```
透過```config/packages/doctrine.yaml```配置更多選項,包括你的```server_version```(例如,如果使用 MySQL 5.7,則為 5.7),這可能會影響 Doctrine 的功能。
:::success
還有許多其他的 Doctrine 指令。
執行 ```php bin/console list doctrine ```以查看完整列表。
:::
## 創建entity class
假設你正在構建一個需要"產品(products)"的應用程式。
甚至不用考慮 Doctrine 或資料庫,你需要一個Product object來表示這些產品。
可以使用```make:entity```指令創建此class以及您需要的任何field。
該指令會問你一些問題 - 像下面那樣回答它們:
```
$ php bin/console make:entity
Class name of the entity to create or update:
> Product
New property name (press <return> to stop adding fields):
> name
Field type (enter ? to see all types) [string]:
> string
Field length [255]:
> 255
Can this field be null in the database (nullable) (yes/no) [no]:
> no
New property name (press <return> to stop adding fields):
> price
Field type (enter ? to see all types) [string]:
> integer
Can this field be null in the database (nullable) (yes/no) [no]:
> no
New property name (press <return> to stop adding fields):
>
(press enter again to finish)
```
現在產生了一個個新```src/Entity/Product.php```文件:
```php=
// src/Entity/Product.php
namespace App\Entity;
use App\Repository\ProductRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=ProductRepository::class)
*/
class Product
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="integer")
*/
private $price;
public function getId(): ?int
{
return $this->id;
}
// ... getter and setter methods
}
```
>note
困惑為什麼價格是整數(integer)?別擔心:這只是一個例子。
但是,將價格存儲為整數(例如 100 = 1 美元)可以避免四捨五入問題。
>note
如果您使用的是 SQLite 數據庫,您將看到以下錯誤: PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL column with default value NULL。
>向```description```屬性添加```nullable=true```選項以解決問題。
:::danger
Caution
在 MySQL 5.6 及更早版本中使用 InnoDB 表時,索引鍵前綴有 767 bytes的限制。
具有 255 個char長度和 utf8mb4 編碼的字串列超過了該限制。
這意味著該類型的任何列```string```,且```unique=true```必須將其最大```length```設定為```190```。
否則,您將看到此錯誤: "[PDOException] SQLSTATE[42000]: Syntax error or access conflict: 1071 Specified key was too long; max key length is 767 bytes"。
:::
這個class被稱為“實體”。
可以將 Product object 保存和查詢到 product 資料庫的表中。
Product entity 中的每個屬性都可以映射到該表中的一列。
這通常通過註釋```@ORM\...```來完成,可以在每個屬性上方看到的註釋:

Doctrine 支持多種field類型,每種類型都有自己的選項。
要查看完整列表,請查看Doctrine 的映射(map)類型[文件](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html)。
如果要使用 XML 而不是註釋,請將```type: xml```和```dir:``` ```'%kernel.project_dir%/config/doctrine'```添加到```config/packages/doctrine.yaml```文件中的entity映射 。
:::danger
Caution
注意不要使用保留的 SQL 關鍵字作為table名稱或列名稱(例如GROUP或USER)。有關如何轉譯這些的詳細訊息,請參閱 Doctrine 的[保留 SQL 關鍵字文件](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html#quoting-reserved-words)。
或者,使用```@ORM\Table(name="groups")```上面的class更改table名稱或使用```name="group_name"```選項配置列名稱。
:::
## Migrations(遷移):創建資料庫表(tables)/架構(schema)
```Product```class是完全配置好並保存到一個```product```表。
如果你剛剛定義了這個class,你的資料庫實際上還沒有這個```product table。
要添加它,可以利用已經安裝的```DoctrineMigrationsBundle```:
```
php bin/console make:migration
```
如果一切正常,您應該會看到如下內容:
```
SUCCESS!
```
下一步:
查看新的Migrations “src/Migrations/Version20180207231217.php”
然後:
使用執行migrations透過
```bin/console:migrations:migrate```
打開此文件,它將包含更新資料庫所需的 SQL!要執行該 SQL,請執行你的migrations:
```
php bin/console doctrine:migrations:migrate
```
此指令執行尚未針對您的資料庫執行的所有migrations文件。
應該在部署你的應用程式時,執行此命令以保持應用程式資料庫是最新的。
## migrations和添加更多field
如果需要向```Product```中添加一個新的field屬性```description```該怎麼做?
可以編輯class以添加新屬性。
也可以再次使用```make:entity```:
```
$ php bin/console make:entity
Class name of the entity to create or update
> Product
New property name (press <return> to stop adding fields):
> description
Field type (enter ? to see all types) [string]:
> text
Can this field be null in the database (nullable) (yes/no) [no]:
> no
New property name (press <return> to stop adding fields):
>
(press enter again to finish)
```
這添加了新的description屬性以及```getDescription()```和```setDescription()```方法:
```php=
// src/Entity/Product.php
// ...
class Product
{
// ...
+ /**
+ * @ORM\Column(type="text")
+ */
+ private $description;
// getDescription() & setDescription() were also added
}
```
新屬性已映射,但尚不存在```product```。
此時可以生成新的migrations:
```
php bin/console make:migration
```
這一次,生成的文件中的 SQL 將如下所示:
```
ALTER TABLE product ADD description LONGTEXT NOT NULL
```
migrations系統是非常聰明的。
它會將所有entities與資料庫的當前狀態進行比較,並生成同步它們所需的 SQL!像以前一樣,執行您的migrations:
```
php bin/console doctrine:migrations:migrate
```
這只會執行一個新的migration文件,因為 ```DoctrineMigrationsBundle``` 知道第一次migrations已經在之前執行過。在系統背後,它管理一個```migration_versions```talbe來追蹤這一點。
每次對架構進行更改時,執行這兩個指令來生成migrations,然後執行它。
確保在部署時提交migrations文件並執行它們。
:::success
如果你更喜歡手動添加新屬性,該```make:entity```指令可以為你生成 ```getter ```和 ```setter```方法:
```
php bin/console make:entity --regenerate
```
如果你進行了一些更改並希望重新生成所有getter/setter 方法,也可以通過```--overwrite```此指令來完成.
:::
## 將object持久化(persisting)到資料庫
將```Product```object保存到資料庫了,透過創建一個新的控制器進行實驗:
```
php bin/console make:controller ProductController
```
在控制器內部,您可以創建一個新Product object ,在其上設置資料並保存:
```php=
// src/Controller/ProductController.php
namespace App\Controller;
// ...
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
/**
* @Route("/product", name="create_product")
*/
public function createProduct(): Response
{
// you can fetch the EntityManager via $this->getDoctrine()
// or you can add an argument to the action: createProduct(EntityManagerInterface $entityManager)
$entityManager = $this->getDoctrine()->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
```
嘗試訪問
http://localhost:8000/product
恭喜!你剛剛在```product ```table中創建了第一行。
為了證明,可以直接查詢資料庫:
```
php bin/console doctrine:query:sql 'SELECT * FROM product'
```
更詳細地看一下前面的例子:
- 第18行的```$this->getDoctrine()->getManager()```方法獲取Doctrine的entity管理object,它是在Doctrine中最重要的object。
它負責將object保存到資料庫,並從資料庫中獲取object。
- 第 20-23 行,將```product```像任何其他 PHP 的object一樣實例化和使用該object。
- 第26行 ,```persist($product)```呼叫(call)告訴 Doctrine“管理” ```product``` object。
這並不會導致要到資料庫中進行查詢(query)。
- 第29行,```flush()```方法被呼叫時,Doctrine 會查看它正在管理的所有object,看看它們是否需要持久保存到資料庫中。
在這個例子中,```product```object的資料在資料庫中不存在,所以entity管理器執行一個```INSERT```查詢,在product table中創建一個新row。
>note
如果```flush()```呼叫失敗,```Doctrine\ORM\ORMException```則拋出異常。請參閱[Transactions和Concurrency](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/transactions-and-concurrency.html)。
無論是在創建還是更新obeject,工作流程總是相同的:Doctrine 足夠聰明,可以知道它是否應該插入或更新entity。
## 驗證對象
Symfony 驗證器會重新使用 Doctrine metadata來執行一些基本的驗證任務:
```php=
// src/Controller/ProductController.php
namespace App\Controller;
use App\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
// ...
class ProductController extends AbstractController
{
/**
* @Route("/product", name="create_product")
*/
public function createProduct(ValidatorInterface $validator): Response
{
$product = new Product();
// This will trigger an error: the column isn't nullable in the database
$product->setName(null);
// This will trigger a type mismatch error: an integer is expected
$product->setPrice('1999');
// ...
$errors = $validator->validate($product);
if (count($errors) > 0) {
return new Response((string) $errors, 400);
}
// ...
}
}
```
儘管```Product```entity 沒有定義任何明顯的驗證配置,Symfony 會在 Doctrine 映射配置來推斷一些驗證規則。
例如,假設該```name```屬性在資料庫中不能為```null```,一個 NotNull 約束會自動添加到該屬性中(如果它不包含該約束)。
下表總結了 Doctrine metadata 和 Symfony 自動添加的相應驗證約束之間的映射:
|doctrine屬性 |驗證約束(validation constraint)| notes|
|---|---|---|
|```nullable=false ```|[NotNull](https://symfony.com/doc/4.4/reference/constraints/NotNull.html)| [需要安裝PropertyInfo 組件](https://symfony.com/doc/4.4/components/property_info.html)|
|```type```| [Type](https://symfony.com/doc/4.4/reference/constraints/Type.html) |[需要安裝PropertyInfo 組件](https://symfony.com/doc/4.4/components/property_info.html)|
|```unique=true``` |[UniqueEntity](https://symfony.com/doc/4.4/reference/constraints/UniqueEntity.html)
|```length``` |[Length](https://symfony.com/doc/4.4/reference/constraints/Length.html) |
因為[Form 組件](https://symfony.com/doc/4.4/forms.html)和[API Platform](https://api-platform.com/docs/core/validation/) 在內部使用 Validator 組件,所以您的所有表單和 Web API 也將自動受益於這些自動驗證約束。
這種自動驗證是一個很好的功能,可以提高您的工作效率,但它並不能完全取代驗證配置。
您仍然需要添加一些驗證約束以確保用戶提供的資料是正確的。
## 從資料庫中獲取object
從資料庫中取回object更容易。假設您希望能夠去```/product/1```查看您的新產品:
```php=
// src/Controller/ProductController.php
namespace App\Controller;
use App\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
// ...
class ProductController extends AbstractController
{
/**
* @Route("/product/{id}", name="product_show")
*/
public function show(int $id): Response
{
$product = $this->getDoctrine()
->getRepository(Product::class)
->find($id);
if (!$product) {
throw $this->createNotFoundException(
'No product found for id '.$id
);
}
return new Response('Check out this great product: '.$product->getName());
// or render a template
// in the template, print things with {{ product.name }}
// return $this->render('product/show.html.twig', ['product' => $product]);
}
}
```
另一種可能性是利用```ProductRepository```來使用 Symfony 的自動裝配功能並由依賴注入(injected by the dependency)來注入容器:
```php=
// src/Controller/ProductController.php
namespace App\Controller;
use App\Entity\Product;
use App\Repository\ProductRepository;
use Symfony\Component\HttpFoundation\Response;
// ...
class ProductController extends AbstractController
{
/**
* @Route("/product/{id}", name="product_show")
*/
public function show(int $id, ProductRepository $productRepository): Response
{
$product = $productRepository
->find($id);
// ...
}
}
```
嘗試訪問!
http://localhost:8000/product/1
當您查詢特定類型的object時,總是使用所謂的“存儲庫(repository)”。可以將存儲庫視為一個 PHP class,它的唯一工作是幫助獲取某個class的entity。
一旦你有了一個存儲庫object,你就有了許多輔助方法:
```php=
$repository = $this->getDoctrine()->getRepository(Product::class);
// look for a single Product by its primary key (usually "id")
$product = $repository->find($id);
// look for a single Product by name
$product = $repository->findOneBy(['name' => 'Keyboard']);
// or find by name and price
$product = $repository->findOneBy([
'name' => 'Keyboard',
'price' => 1999,
]);
// look for multiple Product objects matching the name, ordered by price
$products = $repository->findBy(
['name' => 'Keyboard'],
['price' => 'ASC']
);
// look for *all* Product objects
$products = $repository->findAll();
```
您還可以為更複雜的查詢添加自定義方法!稍後將在資料庫和 Doctrine ORM部分詳細介紹。
:::success
渲染 HTML 頁面時,頁面底部的 Web debug工具欄將顯示查詢的數量和執行它們所花費的時間:

如果資料庫查詢次數過多,該按鍵將變為黃色,表示某些內容可能不正確。
點擊該按鍵以打開 Symfony Profiler 並查看已執行的確切查詢。
如果您沒有看到網頁debug工具,安裝profiler Symfony的[工具](https://symfony.com/doc/4.4/setup.html#symfony-packs)並且執行以下命令:```composer require --dev symfony/profiler-pack```。
:::
## 自動獲取object (ParamConverter)
在很多情況下,您可以使用[SensioFrameworkExtraBundle](https://symfony.com/bundles/SensioFrameworkExtraBundle/current/index.html)自動為您做查詢,可以透過以下指令安裝
```
composer require sensio/framework-extra-bundle
```
現在,簡化你的控制器:
```php=
// src/Controller/ProductController.php
namespace App\Controller;
use App\Entity\Product;
use App\Repository\ProductRepository;
use Symfony\Component\HttpFoundation\Response;
// ...
class ProductController extends AbstractController
{
/**
* @Route("/product/{id}", name="product_show")
*/
public function show(Product $product): Response
{
// use the Product!
// ...
}
}
```
bundle所使用的```{id}```是來自於路由的查詢來自於```Product```的```id```column。
如果未找到,則會生成 404 頁面。
您可以使用更多選項。
閱讀有關[ParamConverter](https://symfony.com/bundles/SensioFrameworkExtraBundle/current/annotations/converters.html)的文件。
## 更新obeject
一旦你從 Doctrine 獲取了一個object,就可以像與任何 PHP model一樣與它進行溝通:
```php=
// src/Controller/ProductController.php
namespace App\Controller;
use App\Entity\Product;
use App\Repository\ProductRepository;
use Symfony\Component\HttpFoundation\Response;
// ...
class ProductController extends AbstractController
{
/**
* @Route("/product/edit/{id}")
*/
public function update(int $id): Response
{
$entityManager = $this->getDoctrine()->getManager();
$product = $entityManager->getRepository(Product::class)->find($id);
if (!$product) {
throw $this->createNotFoundException(
'No product found for id '.$id
);
}
$product->setName('New product name!');
$entityManager->flush();
return $this->redirectToRoute('product_show', [
'id' => $product->getId()
]);
}
}
```
使用 Doctrine 編輯現有product包括三個步驟:
1. 從 Doctrine 中獲取對象;
2. 修改對象;
3. 呼叫```flush()```entity管理器。
可以呼叫```$entityManager->persist($product)```,但這不是必需的:Doctrine 已經在“觀察”您的object的變化。
## 刪除object
需要呼叫```remove()``` entity管理器的方法:
```php=
$entityManager->remove($product);
$entityManager->flush();
```
該```remove()```方法通知 Doctrine 您想從資料庫中刪除指定的object。在呼叫```DELETE```查詢在呼叫```flush()```方法前,不會實際執行查詢。
## 查詢對象:存儲庫(repository)
您已經看到存儲庫object如何讓你無需任何動作即可運行基本查詢:
```php=
// from inside a controller
$repository = $this->getDoctrine()->getRepository(Product::class);
$product = $repository->find($id);
```
但是,如果需要更複雜的查詢怎麼辦?當您使用```make:entity```生成entity時,該命令還生成了一個```ProductRepository```class:
```php=
// src/Repository/ProductRepository.php
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
}
```
當您獲取存儲庫(即```->getRepository(Product::class)```)時,它 實際上是該object的一個實例!這是因為```repositoryClass```在```Product```entity class頂部生成的配置。
假設您要查詢大於某個價格的所有```Product```object。
向你的存儲庫添加一個新方法:
```php=
// src/Repository/ProductRepository.php
// ...
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
/**
* @return Product[]
*/
public function findAllGreaterThanPrice(int $price): array
{
$entityManager = $this->getEntityManager();
$query = $entityManager->createQuery(
'SELECT p
FROM App\Entity\Product p
WHERE p.price > :price
ORDER BY p.price ASC'
)->setParameter('price', $price);
// returns an array of Product objects
return $query->getResult();
}
}
```
傳遞給的字串```createQuery()```可能看起來像 SQL,但它是 Doctrine Query Language。
這允許你使用一般的的查詢語言(query language)來輸入查詢,但改為引用 PHP object來取代(即在```FROM```語句中)。
現在,您可以在存儲庫上呼叫此方法:
```php=
// from inside a controller
$minPrice = 1000;
$products = $this->getDoctrine()
->getRepository(Product::class)
->findAllGreaterThanPrice($minPrice);
// ...
```
有關如何將存儲庫注入任何服務的資訊,請參閱[service container](https://symfony.com/doc/4.4/service_container.html#services-constructor-injection)。
### 使用查詢生成器(query builder)查詢
Doctrine 還提供了一個[Query Builder](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/query-builder.html),一種針對物件導向的查詢方式。
建議在動態構建查詢時使用(基於PHP條件的情況下):
```php=
// src/Repository/ProductRepository.php
// ...
class ProductRepository extends ServiceEntityRepository
{
public function findAllGreaterThanPrice(int $price, bool $includeUnavailableProducts = false): array
{
// automatically knows to select Products
// the "p" is an alias you'll use in the rest of the query
$qb = $this->createQueryBuilder('p')
->where('p.price > :price')
->setParameter('price', $price)
->orderBy('p.price', 'ASC');
if (!$includeUnavailableProducts) {
$qb->andWhere('p.available = TRUE');
}
$query = $qb->getQuery();
return $query->execute();
// to get just one result:
// $product = $query->setMaxResults(1)->getOneOrNullResult();
}
}
```
### 使用 SQL 查詢
另外,如果需要,可以直接用SQL查詢:
```php=
// src/Repository/ProductRepository.php
// ...
class ProductRepository extends ServiceEntityRepository
{
public function findAllGreaterThanPrice(int $price): array
{
$conn = $this->getEntityManager()->getConnection();
$sql = '
SELECT * FROM product p
WHERE p.price > :price
ORDER BY p.price ASC
';
$stmt = $conn->prepare($sql);
$stmt->execute(['price' => $price]);
// returns an array of arrays (i.e. a raw data set)
return $stmt->fetchAllAssociative();
}
}
```
使用 SQL,您將回傳原始資料,而不是object(除非您使用[NativeQuery](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/native-sql.html)功能)。
## 配置
請參閱[Doctrine配置](https://symfony.com/doc/4.4/reference/configuration/doctrine.html)參考。
## 關係(relationships)和關聯(associations)
Doctrine 提供了管理數據庫關係(也稱為關聯)所需的所有功能,包括 ManyToOne、OneToMany、OneToOne 和 ManyToMany 關係。
相關資訊,請參閱此[文件](https://symfony.com/doc/4.4/doctrine/associations.html)。
## 資料庫測試
閱讀此[文件](https://symfony.com/doc/4.4/testing/database.html)。