Doctrine ORM:Symfony中的数据库持久层

Doctrine ORM:Symfony 中的数据库持久层,一场关于数据的华丽冒险

各位观众,各位亲爱的代码爱好者们,欢迎来到本次的“数据持久化奇妙夜”! 今晚,我们将共同探索 Symfony 框架中那颗璀璨的明星——Doctrine ORM! 准备好迎接一场关于数据的华丽冒险了吗? 让我们一起揭开它的神秘面纱,看看它如何将数据库操作变得优雅、高效,甚至……充满乐趣!

第一幕:什么是 ORM?为什么我们需要它?

想象一下,你是一位才华横溢的小说家,每天都在用文字编织着精彩的故事。 但是,你发现自己每天都要花大量的时间去整理稿件,排版,校对,甚至还要自己去印刷厂排队! 哎,创作的激情都被这些琐事磨灭了。

ORM (Object-Relational Mapper),就是你的救星! 它可以让你专注于写作 (业务逻辑),而把那些繁琐的排版、校对、印刷 (数据库操作) 交给它来处理。

什么是 ORM 呢?

简单来说,ORM 是一种技术,它允许你使用面向对象的方式来操作数据库。 它就像一个翻译官,将你写的面向对象的代码 (比如 PHP 中的对象) 翻译成数据库可以理解的 SQL 语句,然后再将数据库返回的结果翻译成你可以直接使用的对象。

为什么我们需要 ORM 呢?

  • 解放双手,专注业务: 告别手动编写 SQL 语句的痛苦,将更多精力投入到业务逻辑的实现上。 这就像拥有了一个超级助手,帮你处理所有繁琐的数据库操作。
  • 代码可读性更高: 使用 ORM 可以让你的代码更加简洁、易懂。 不再需要面对那些冗长而复杂的 SQL 语句,只需要操作对象即可。
  • 数据库迁移更轻松: 当你的项目需要更换数据库时,使用 ORM 可以大大简化迁移过程。 你只需要修改一些配置,而不需要修改大量的 SQL 语句。 这就像拥有了一张神奇的地图,可以轻松地带你到达新的目的地。
  • 安全性更高: ORM 通常会提供一些安全机制,例如防止 SQL 注入等,从而提高你的应用程序的安全性。
特性 ORM 的优势
开发效率 显著提高,避免重复编写 SQL,专注于业务逻辑
代码可维护性 代码更简洁、易读,方便维护和修改
数据库可移植性 轻松切换数据库,减少修改代码的工作量
安全性 提供安全机制,防止 SQL 注入等安全问题
测试友好性 更容易进行单元测试和集成测试,因为可以模拟 ORM 的行为

第二幕:Doctrine ORM,Symfony 的最佳搭档

在众多 ORM 框架中,Doctrine ORM 无疑是 Symfony 框架的最佳搭档。 它们就像一对天作之合,共同构建出一个强大而优雅的 Web 开发平台。

Doctrine ORM 的特点:

  • 功能强大: Doctrine ORM 提供了丰富的功能,包括对象关系映射、数据验证、事务管理、缓存等等。 几乎你能想到的数据库操作,它都能帮你搞定。
  • 灵活可配置: Doctrine ORM 允许你根据自己的需求进行灵活配置。 你可以自定义数据类型、映射关系、查询方式等等。 这就像拥有了一块万能的积木,可以搭建出各种各样的模型。
  • 性能优秀: Doctrine ORM 经过了精心的优化,可以保证你的应用程序具有良好的性能。 它提供了多种缓存机制,可以减少数据库的访问次数。
  • 社区活跃: Doctrine ORM 拥有一个庞大而活跃的社区,你可以轻松地找到各种各样的资源和帮助。

如何在 Symfony 中使用 Doctrine ORM 呢?

首先,你需要安装 DoctrineBundle:

composer require doctrine/orm doctrine/doctrine-bundle

安装完成后,你需要配置 config/packages/doctrine.yaml 文件,指定数据库连接信息:

doctrine:
    dbal:
        driver:   pdo_mysql
        host:     '%env(DATABASE_HOST)%'
        port:     '%env(DATABASE_PORT)%'
        dbname:   '%env(DATABASE_NAME)%'
        user:     '%env(DATABASE_USER)%'
        password: '%env(DATABASE_PASSWORD)%'
        charset:  UTF8

    orm:
        auto_generate_proxy_classes: true
        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
        auto_mapping: true
        mappings:
            App:
                is_bundle: false
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'AppEntity'
                alias: App

第三幕:实体 (Entity),你的数据模型

在 Doctrine ORM 中,实体 (Entity) 是你的数据模型,它代表了数据库中的一张表。 每个实体类对应一张表,每个实体类的属性对应表中的一个字段。

如何创建一个实体呢?

你可以使用 Symfony 的 MakerBundle 来创建一个实体:

php bin/console make:entity Product

这个命令会创建一个 src/Entity/Product.php 文件,并且会提示你添加属性。

<?php

namespace AppEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity(repositoryClass="AppRepositoryProductRepository")
 */
class Product
{
    /**
     * @ORMId()
     * @ORMGeneratedValue()
     * @ORMColumn(type="integer")
     */
    private $id;

    /**
     * @ORMColumn(type="string", length=255)
     */
    private $name;

    /**
     * @ORMColumn(type="text", nullable=true)
     */
    private $description;

    /**
     * @ORMColumn(type="decimal", precision=10, scale=2)
     */
    private $price;

    // Getters and setters...
}

解释一下上面的代码:

  • @ORMEntity(repositoryClass="AppRepositoryProductRepository"): 这个注解告诉 Doctrine ORM,这是一个实体类,并且指定了对应的 Repository 类。
  • @ORMId(): 这个注解表示 id 属性是主键。
  • @ORMGeneratedValue(): 这个注解表示 id 属性的值由数据库自动生成。
  • @ORMColumn(type="integer"): 这个注解表示 id 属性对应数据库中的一个整数类型的字段。
  • @ORMColumn(type="string", length=255): 这个注解表示 name 属性对应数据库中的一个字符串类型的字段,长度为 255。
  • @ORMColumn(type="text", nullable=true): 这个注解表示 description 属性对应数据库中的一个文本类型的字段,允许为空。
  • @ORMColumn(type="decimal", precision=10, scale=2): 这个注解表示 price 属性对应数据库中的一个十进制类型的字段,精度为 10,小数位数为 2。

实体与数据库表的关系:

实体属性 (PHP) 数据库字段 (MySQL) Doctrine 类型
$id id integer
$name name string
$description description text
$price price decimal

创建数据库表:

创建完实体类后,你需要执行以下命令来创建数据库表:

php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate

doctrine:migrations:diff 命令会生成一个数据库迁移文件,描述了如何创建数据库表。 doctrine:migrations:migrate 命令会执行这个迁移文件,从而创建数据库表。

第四幕:Repository,你的数据访问层

Repository 是你的数据访问层,它负责与数据库进行交互。 你可以使用 Repository 来查询、创建、更新、删除实体。

如何创建一个 Repository 呢?

当你创建实体类时,MakerBundle 会自动生成一个 Repository 类。 你也可以手动创建一个 Repository 类,并将其与实体类关联起来。

<?php

namespace AppRepository;

use AppEntityProduct;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrinePersistenceManagerRegistry;

/**
 * @method Product|null find($id, $lockMode = null, $lockVersion = null)
 * @method Product|null findOneBy(array $criteria, array $orderBy = null)
 * @method Product[]    findAll()
 * @method Product[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }

    // /**
    //  * @return Product[] Returns an array of Product objects
    //  */
    /*
    public function findByExampleField($value)
    {
        return $this->createQueryBuilder('p')
            ->andWhere('p.exampleField = :val')
            ->setParameter('val', $value)
            ->orderBy('p.id', 'ASC')
            ->setMaxResults(10)
            ->getQuery()
            ->getResult()
        ;
    }
    */

    /*
    public function findOneBySomeField($value): ?Product
    {
        return $this->createQueryBuilder('p')
            ->andWhere('p.exampleField = :val')
            ->setParameter('val', $value)
            ->getQuery()
            ->getOneOrNullResult()
        ;
    }
    */
}

Repository 的常用方法:

  • find($id): 根据 ID 查找实体。
  • findOneBy(array $criteria, array $orderBy = null): 根据条件查找单个实体。
  • findAll(): 查找所有实体。
  • findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): 根据条件查找多个实体。
  • createQueryBuilder($alias): 创建一个 QueryBuilder 对象,用于构建复杂的查询。

使用 Repository 进行数据库操作:

<?php

namespace AppController;

use AppEntityProduct;
use AppRepositoryProductRepository;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;

class ProductController extends AbstractController
{
    /**
     * @Route("/product", name="product_index")
     */
    public function index(ProductRepository $productRepository): Response
    {
        $products = $productRepository->findAll();

        return $this->render('product/index.html.twig', [
            'products' => $products,
        ]);
    }

    /**
     * @Route("/product/{id}", name="product_show")
     */
    public function show(Product $product): Response
    {
        return $this->render('product/show.html.twig', [
            'product' => $product,
        ]);
    }

    /**
     * @Route("/product/new", name="product_new")
     */
    public function new(): Response
    {
        $product = new Product();
        $product->setName('New Product');
        $product->setDescription('This is a new product.');
        $product->setPrice(99.99);

        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->persist($product);
        $entityManager->flush();

        return new Response('Saved new product with id '.$product->getId());
    }
}

解释一下上面的代码:

  • $productRepository->findAll(): 调用 ProductRepositoryfindAll() 方法,查找所有产品。
  • $entityManager = $this->getDoctrine()->getManager(): 获取 Doctrine 的 EntityManager 对象。
  • $entityManager->persist($product): 将 $product 对象添加到 EntityManager 中,表示要保存这个对象。
  • $entityManager->flush(): 将 EntityManager 中的所有更改同步到数据库。

第五幕:QueryBuilder,你的 SQL 魔法棒

QueryBuilder 是 Doctrine ORM 提供的一个强大的工具,它可以让你以面向对象的方式构建复杂的 SQL 查询。 就像一个魔法棒,它可以让你轻松地实现各种各样的查询需求。

如何使用 QueryBuilder 呢?

<?php

namespace AppRepository;

use AppEntityProduct;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrinePersistenceManagerRegistry;

/**
 * @method Product|null find($id, $lockMode = null, $lockVersion = null)
 * @method Product|null findOneBy(array $criteria, array $orderBy = null)
 * @method Product[]    findAll()
 * @method Product[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }

    /**
     * @return Product[] Returns an array of Product objects
     */
    public function findByPriceGreaterThan($price)
    {
        return $this->createQueryBuilder('p')
            ->andWhere('p.price > :price')
            ->setParameter('price', $price)
            ->orderBy('p.name', 'ASC')
            ->getQuery()
            ->getResult()
        ;
    }

    /*
    public function findOneBySomeField($value): ?Product
    {
        return $this->createQueryBuilder('p')
            ->andWhere('p.exampleField = :val')
            ->setParameter('val', $value)
            ->getQuery()
            ->getOneOrNullResult()
        ;
    }
    */
}

解释一下上面的代码:

  • $this->createQueryBuilder('p'): 创建一个 QueryBuilder 对象,'p' 是实体 Product 的别名。
  • ->andWhere('p.price > :price'): 添加一个 WHERE 条件,p.price > :price 表示 price 属性大于 :price 参数。
  • ->setParameter('price', $price): 设置 :price 参数的值。
  • ->orderBy('p.name', 'ASC'): 按照 name 属性升序排序。
  • ->getQuery(): 将 QueryBuilder 对象转换为 Query 对象。
  • ->getResult(): 执行查询,并返回结果。

QueryBuilder 的常用方法:

  • select($select): 指定要查询的字段。
  • from($entityName, $alias): 指定要查询的实体和别名。
  • where($where): 添加 WHERE 条件。
  • andWhere($andWhere): 添加 AND 条件。
  • orWhere($orWhere): 添加 OR 条件。
  • setParameter($parameter, $value): 设置参数的值。
  • orderBy($sort, $order = null): 按照指定的字段排序。
  • groupBy($groupBy): 按照指定的字段分组。
  • having($having): 添加 HAVING 条件。
  • setMaxResults($maxResults): 限制结果的数量。
  • setFirstResult($firstResult): 设置结果的起始位置。

第六幕:Associations,实体之间的关系

在现实世界中,实体之间通常存在着各种各样的关系。 例如,一个用户可以拥有多个订单,一个订单可以包含多个产品。 Doctrine ORM 提供了 Associations 功能,可以让你轻松地定义实体之间的关系。

Doctrine ORM 支持以下几种 Associations:

  • One-to-One (一对一): 一个实体只能关联到另一个实体。 例如,一个用户只能拥有一个个人资料。
  • One-to-Many (一对多): 一个实体可以关联到多个实体。 例如,一个用户可以拥有多个订单。
  • Many-to-One (多对一): 多个实体可以关联到同一个实体。 例如,多个订单可以属于同一个用户。
  • Many-to-Many (多对多): 多个实体可以关联到多个实体。 例如,多个产品可以属于多个类别。

如何定义 Associations 呢?

<?php

namespace AppEntity;

use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
use DoctrineORMMapping as ORM;

/**
 * @ORMEntity(repositoryClass="AppRepositoryUserRepository")
 */
class User
{
    /**
     * @ORMId()
     * @ORMGeneratedValue()
     * @ORMColumn(type="integer")
     */
    private $id;

    /**
     * @ORMColumn(type="string", length=255)
     */
    private $email;

    /**
     * @ORMOneToMany(targetEntity="AppEntityOrder", mappedBy="user")
     */
    private $orders;

    public function __construct()
    {
        $this->orders = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;

        return $this;
    }

    /**
     * @return Collection|Order[]
     */
    public function getOrders(): Collection
    {
        return $this->orders;
    }

    public function addOrder(Order $order): self
    {
        if (!$this->orders->contains($order)) {
            $this->orders[] = $order;
            $order->setUser($this);
        }

        return $this;
    }

    public function removeOrder(Order $order): self
    {
        if ($this->orders->contains($order)) {
            $this->orders->removeElement($order);
            // set the owning side to null (unless already changed)
            if ($order->getUser() === $this) {
                $order->setUser(null);
            }
        }

        return $this;
    }
}
<?php

namespace AppEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity(repositoryClass="AppRepositoryOrderRepository")
 */
class Order
{
    /**
     * @ORMId()
     * @ORMGeneratedValue()
     * @ORMColumn(type="integer")
     */
    private $id;

    /**
     * @ORMManyToOne(targetEntity="AppEntityUser", inversedBy="orders")
     * @ORMJoinColumn(nullable=false)
     */
    private $user;

    /**
     * @ORMColumn(type="datetime")
     */
    private $createdAt;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function setUser(?User $user): self
    {
        $this->user = $user;

        return $this;
    }

    public function getCreatedAt(): ?DateTimeInterface
    {
        return $this->createdAt;
    }

    public function setCreatedAt(DateTimeInterface $createdAt): self
    {
        $this->createdAt = $createdAt;

        return $this;
    }
}

解释一下上面的代码:

  • @ORMOneToMany(targetEntity="AppEntityOrder", mappedBy="user"): 这个注解表示 User 实体和 Order 实体之间存在 One-to-Many 关系。 targetEntity 指定了关联的实体类,mappedBy 指定了关联的反向关系。
  • @ORMManyToOne(targetEntity="AppEntityUser", inversedBy="orders"): 这个注解表示 Order 实体和 User 实体之间存在 Many-to-One 关系。 targetEntity 指定了关联的实体类,inversedBy 指定了关联的反向关系。
  • @ORMJoinColumn(nullable=false): 这个注解表示 user 属性是外键,并且不能为空。

使用 Associations 进行数据操作:

<?php

namespace AppController;

use AppEntityUser;
use AppEntityOrder;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;

class OrderController extends AbstractController
{
    /**
     * @Route("/order/new", name="order_new")
     */
    public function new(): Response
    {
        $user = new User();
        $user->setEmail('[email protected]');

        $order = new Order();
        $order->setCreatedAt(new DateTime());
        $order->setUser($user);

        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->persist($user);
        $entityManager->persist($order);
        $entityManager->flush();

        return new Response('Saved new order with id '.$order->getId());
    }
}

第七幕:Doctrine Data Fixtures,数据的初始化助手

Doctrine Data Fixtures 是一个用于初始化数据库数据的工具。 当你开发一个新项目时,或者当你需要重置数据库时,可以使用 Data Fixtures 来快速地填充一些测试数据。

如何使用 Doctrine Data Fixtures 呢?

首先,你需要安装 DoctrineFixturesBundle:

composer require doctrine/doctrine-fixtures-bundle --dev

安装完成后,你需要创建一个 Fixture 类:

<?php

namespace AppDataFixtures;

use AppEntityProduct;
use DoctrineBundleFixturesBundleFixture;
use DoctrinePersistenceObjectManager;

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        for ($i = 0; $i < 10; $i++) {
            $product = new Product();
            $product->setName('Product '.$i);
            $product->setDescription('This is product '.$i);
            $product->setPrice(rand(10, 100));
            $manager->persist($product);
        }

        $manager->flush();
    }
}

解释一下上面的代码:

  • AppFixtures 类继承自 Fixture 类。
  • load(ObjectManager $manager) 方法负责创建和保存数据。
  • $manager->persist($product): 将 $product 对象添加到 EntityManager 中,表示要保存这个对象。
  • $manager->flush(): 将 EntityManager 中的所有更改同步到数据库。

加载 Data Fixtures:

php bin/console doctrine:fixtures:load

这个命令会加载所有的 Fixture 类,并将数据保存到数据库中。

终幕:总结与展望

恭喜你! 经过这一系列的冒险,你已经对 Doctrine ORM 有了深入的了解。 你现在已经掌握了如何使用 Doctrine ORM 来进行数据库操作,并且可以构建出更加强大而优雅的 Symfony 应用程序。

Doctrine ORM 是一个功能强大而灵活的工具,它可以帮助你解决各种各样的数据库问题。 掌握它,你就可以更加专注于业务逻辑的实现,从而提高你的开发效率。

当然,Doctrine ORM 的世界还有很多值得探索的地方。 例如,你可以学习如何使用 Doctrine 的 Event Listeners 和 Lifecycle Callbacks 来实现更加复杂的业务逻辑,或者你可以学习如何使用 Doctrine 的 DQL (Doctrine Query Language) 来编写更加灵活的查询。

希望你在数据持久化的道路上越走越远,创造出更加精彩的作品! 感谢大家的观看,我们下期再见! 👋

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注