Ecotone:使处理数据库变得简单

admin admin 2024-01-24 139 阅读 0 评论

在处理数据时,我们经常需要将类映射到数据库表,反之亦然。这是为了将数据库中的简单标量类型映射到更高级别的对象。

映射、获取和存储数据是与业务逻辑无关的低级操作。因此,我们通常将其隐藏在接口后面,例如 DAO 或存储库模式。

为了完全专注于业务,我们需要尽量减少处理低级代码。这需要我们将整个应用程序视为面向业务的整体,而不是仅仅关注特定的层或模块。

PHP 方式与业务接口

Ecotone 的业务接口旨在减少低级和样板代码,让开发人员能够专注于更重要的业务逻辑。

Ecotone 还提供了用于数据库访问的特殊类型的业务接口。这些接口消除了转换逻辑、参数绑定和 SQL 执行的需要。通过这种方式,开发人员可以将低级代码隐藏在抽象背后,专注于业务逻辑。

修改数据库数据

为了定义插入、更新或删除数据库记录的方法,我们可以使用 Ecotone 的 DbalWrite 属性。

<?php

interface PersonService
{
    #[DbalWrite('INSERT INTO persons VALUES (:name, :surname)')]
    public function register(string $name, string $surname): void;
}

该属性将告诉 Ecotone 在调用此方法时执行给定的 SQL。该接口的实现将由 Ecotone 交付并在您的依赖容器中注册。

在上述示例中,我们创建了一个 PersonService 接口,其中包含一个 register() 方法。该方法将新记录插入到 persons 表中。

我们使用 DbalWrite 属性将 INSERT INTO persons VALUES (:name, :surname) 绑定到 register() 方法。这告诉 Ecotone 在调用 register() 方法时执行此 SQL

register() 方法的参数 name 和 surname 将自动绑定到 SQL 参数 :name 和 :surname

返回修改记录数

在更新或删除记录时,我们可能需要知道有多少记录被修改。为此,我们可以为声明的方法添加 Integer 返回类型。

<?php

interface PersonService
{
    #[DbalWrite('UPDATE activities SET active = false WHERE last_activity < :activityOlderThan')]
    public function markAsInactive(\DateTimeImmutable $activityOlderThan): int;
}

如您所见,我们使用了 DateTimeImmutable 作为参数。Ecotone 将使用内置转换,在执行 SQL 之前将日期时间转换为字符串。

参数转换

领域模型通常使用比数据库理解的标量类型更高级别的类。在大多数情况下,我们希望接口遵循业务类型而不是数据库类型。为此,我们可以使用转换机制。

内置类转换

Ecotone 提供了默认的日期时间转换,但它也可以为任何提供 __toString() 方法的类提供转换。

例如,以下示例类可以通过 __toString() 方法自动转换:

<?php

final readonly class PersonId
{
    public function __construct(public string $id) {}

    public function __toString(): string
    {
        return $this->id;
    }
}

现在我们可以将它用作接口的一部分,而不必担心转换:

<?php

interface PersonService
{
    #[DbalWrite('INSERT INTO activities VALUES (:personId, :activity, :time)')]
    public function store(PersonId $personId, string $activity, \DateTimeImmutable $time): void;
}

在这种情况下,PersonId 和 DateTimeImmutable 都会自动转换。PersonId 将自动转换为字符串,因为它包含 __toString() 方法。

定制参数转换

我们可以编写自己的转换器来定制给定类的转换方式。例如,假设我们有一个 DayOfWeek 类,它在 PHP 级别上表示为枚举字符串,但在数据库中我们希望将其存储为整数。

<?php

enum DayOfWeek: string
{
    case MONDAY = 'monday';
    case TUESDAY = 'tuesday';
    case WEDNESDAY = 'wednesday';
    case THURSDAY = 'thursday';
    case FRIDAY = 'friday';
    case SATURDAY = 'saturday';
    case SUNDAY = 'sunday';

    public function toNumber(): int
    {
        return match ($this) {
            self::MONDAY => 1,
            self::TUESDAY => 2,
            self::WEDNESDAY => 3,
            self::THURSDAY => 4,
            self::FRIDAY => 5,
            self::SATURDAY => 6,
            self::SUNDAY => 7,
        };
    }
}

在这种情况下,我们需要将 DayOfWeek 转换为整数。为此,我们可以编写一个 DayOfWeekConverter 类:

<?php

final readonly class DayOfWeekConverter
{
    #[Converter]
    public function dayToNumber(DayOfWeek $day): int
    {
        return $day->toNumber();
    }
}

Converter 是一个注册在依赖容器中的类。Ecotone 将通过属性标记找到所有转换器,并在需要转换时调用它。在我们的例子中,当需要从 DayOfWeek 转换为整数时,它将被调用。

定义转换器后,我们现在可以在接口中使用 DayOfWeek 类,并确保在数据库中它将作为整数存储。

<?php

interface Scheduler
{
    #[DbalWrite('INSERT INTO schedule (day, task) VALUES (:day, :task)')]
    public function scheduleForDayOfWeek(DayOfWeek $day, string $task): void;
}

转换器将在您的所有接口之间重用,因此我们只需编写一次即可涵盖所有情况。

使用表达式语言

表达式语言可用于为给定场景自定义参数。这使我们可以自定义特定操作的行为。

例如,假设我们有一个 PersonName 类,我们想在保存之前将其转换为小写。

<?php

final readonly class PersonName
{
    public function __construct(
        public string $name
    ) {
    }

    public function toLowerCase(): string
    {
        return strtolower($this->name);
    }
}

为了将 PersonName 存储为小写,我们可以使用表达式语言在保存之前调用此方法:

<?php

interface PersonService
{
    #[DbalWrite('INSERT INTO persons VALUES (:personId, :name)')]
    public function register(
        int $personId,
        #[DbalParameter(expression: 'payload.toLowerCase()')] PersonName $name
    ): void;
}

在将 PersonName 存储到数据库之前调用 toLowerCase()

通过提供 DbalParameter 属性,我们可以定义在存储给定参数之前要计算的表达式。

payload 是表达式中引用给定参数的特殊变量,在本例中为 PersonName

非参数方法

我们可能会遇到不需要传递参数的情况,因为它可以动态评估。为此,我们可以使用 DbalParameter 作为方法属性的一部分。

<?php

interface PersonService 
{
    #[DbalWrite('INSERT INTO persons VALUES (:personId, :name, :now)')]
    #[DbalParameter(name: 'now', expression: "reference('clock').now()"] 
    public function register(
        int $personId,
        PersonName $name
    ): void; 
}

在这种情况下,我们在方法级别使用 Dbal 参数预定义了“now”参数。我们使用表达式语言来评估参数值。

reference() 是表达式中的一个特殊变量,它指向您的依赖容器。这样我们就可以获取给定的服务并直接调用它的方法。在这种情况下,我们正在获取时钟服务并调用 now() 方法。

对于方法级 Dbal 参数,我们可以通过名称访问传递给方法的所有参数。在我们的例子中,它将是“personId”或“name”。

基于 JSON 的数据库参数

数据库列并不总是包含简单的标量类型,它实际上可能包含 JSON。然而,在我们的域级代码中,JSON 主要表示为更复杂的类或对象数组,因此需要转换。

假设我们要在数据库中存储人员角色数组。在域级代码中,人员角色表示为 PersonRole 类。

<?php

final readonly class PersonRole
{
    public function __construct(public string $role) {}

    public function getRole(): string
    {
        return $this->role;
    }
}

然后,在接口级别,我们将定义角色数组:

<?php

interface PersonService 
{
    /**
     * @param PersonRole[] $roles
     */
    #[DbalWrite('INSERT INTO persons VALUES (:personId, :roles)')]
    public function addRoles(
        int $personId,
        #[DbalParameter(convertToMediaType: MediaType::APPLICATION_JSON)] array $roles
    ): void; 
}

通过使用 ConvertToMediaType 定义 DbalParameter,我们声明我们希望将给定参数转换为特定媒体类型,在我们的例子中它将是 JSON

我们可以注册自己的 Media Type Converter,但如果我们使用开箱即用的 JMS Module,我们只需要定义一个 Converter

<?php

final class PersonRoleConverter
{
    #[Converter]
    public function from(PersonRole $personRole): string
    {
        return $personRole->getRole();
    }
}

这足以将 PersonRoles 集合直接转换为 JSON

查询数据库数据

到目前为止,我们专注于存储数据和参数转换。但是,我们还可以使用 Ecotone 的抽象来查询数据。

查询多条记录

为了获取多条记录,我们可以使用 DbalQuery 属性。该属性指定 SQL 查询,Ecotone 将使用它来查询数据库。

<?php

interface PersonService
{
    #[DbalQuery('SELECT person_id, name FROM persons LIMIT :limit OFFSET :offset')]
    public function getPersons(int $limit, int $offset): array;
}

这将创建一个 getPersons() 方法,它将返回包含 person_id 和 name 的数组的数组。

我们还可以将 Pagination 对象作为参数传递,以使界面更具可读性。Pagination 对象包含 limit 和 offset 属性。

<?php

final readonly class Pagination
{
    public function __construct(public int $limit, public int $offset)
    {
    }
}

然后,我们可以使用 Pagination 对象作为 getPersons() 方法的参数:

<?php

interface PersonService
{
    #[DbalQuery('SELECT person_id, name FROM persons LIMIT :(pagination.limit) OFFSET :(pagination.offset)')]
    public function getNameListWithIgnoredParameters(
        Pagination $pagination
    ): array;
}

为了在 SQL 中使用表达式语言,我们可以使用 : 运算符。例如,如果我们想访问 Pagination 对象的 limit 属性,我们可以使用以下表达式:

:(pagination.limit)

转换结果集

在处理域级代码时,我们通常希望使用类而不是 Dbal 默认返回的关联数组。Ecotone 可以根据接口中定义的返回类型转换结果。

查询单个记录并将其转换为 PersonDTO

例如,假设我们有一个 PersonService 接口,它定义了一个 get() 方法,该方法返回一个 PersonDTO 对象。

<?php

interface PersonService
{
      #[DbalQuery(
        'SELECT person_id, name FROM persons WHERE person_id = :personId',
        fetchMode: FetchMode::FIRST_ROW
    )]
    public function get(int $personId): PersonDTO; 
}

在这种情况下,我们使用 fetchMode 属性声明我们想要从结果中获取单行。然后,Ecotone 将调用 PersonDTOConverter 类将结果转换为 PersonDTO 对象。

<?php

class PersonDTOConverter
{
    #[Converter]
    public function to(array $personDTO): PersonDTO
    {
        return new PersonNameDTO($personDTO['person_id'], $personDTO['name']);
    }
}

PersonDTOConverter 类将从结果中获取 person_id 和 name 字段,并将它们用于构造 PersonDTO 对象。

返回空值

当获取单行时,我们可能根本找不到结果。对于这种情况,我们可以使用联合返回类型。

<?php

interface PersonService
{
      #[DbalQuery(
        'SELECT person_id, name FROM persons WHERE person_id = :personId',
        fetchMode: FetchMode::FIRST_ROW
    )]
    public function get(int $personId): PersonDTO|null; 
}

在这种情况下,如果找到记录,Ecotone 将返回 PersonDTO 对象。如果没有找到记录,Ecotone 将返回 null

返回单个值

对于像 SUM()COUNT()MIN() 这样的聚合函数,我们可能希望直接返回它们而不是特定的行。为此,Ecotone 提供了获取模式来返回第一行的第一列。

<?php

interface PersonService
{
    #[DbalQuery(
        'SELECT COUNT(*) FROM persons',
        fetchMode: FetchMode::FIRST_COLUMN_OF_FIRST_ROW
    )]
    public function countPersons(): int;
}

在这种情况下,Ecotone 将直接返回聚合函数的值,而不是整个结果集。

转换多条记录

当获取多行时,我们也可能希望使用类而不是数组。然而,我们需要定义我们应该返回什么,并且 PHP 不支持泛型。为了解决这个缺失的功能,Ecotone 提供了读取 Docblock 的能力,以便了解我们想要转换成什么。

<?php

interface PersonService
{
    /**
    * @return PersonDTO[]
    */
    #[DbalQuery(
      'SELECT person_id, name FROM persons LIMIT :limit OFFSET :offset'
    )]
    public function get(int $limit, int $offset)): array; 
}

在这种情况下,Ecotone 将读取 Docblock,并将结果转换为 PersonDTO 的数组。

获取大型结果集

获取关联结果的默认模式是将所有结果都加载到内存中。然而,对于大型结果集,这可能会导致内存不足的问题。为了解决这个问题,我们可以使用 fetch() 模式来迭代结果。

<?php

interface PersonService
{
    /**
    * @return iterable<PersonDTO>
    */
    #[DbalQuery(
       'SELECT person_id, name FROM persons',
       fetchMode: FetchMode::ITERATE
    )]
    public function getAll(): iterable;
}

在这种情况下,Ecotone 将一次只加载和转换一行,确保内存使用安全。

Doctrine ORM 支持

如果我们使用 Ecotone 的聚合支持与 Doctrine ORM,我们可以使用特殊类型的业务接口 - 存储库。存储库接口允许我们使用 Doctrine ORM 来访问和管理数据库中的实体。

<?php

interface PersonRepository
{
    #[Repository]
    public function get(int $personId): ?Person;

    #[Repository]
    public function save(Person $person): void;
}

Repository 注解告诉 Ecotone 该接口是存储库。Ecotone 将使用 getRepository() 方法找到相关的 Doctrine ORM 实体管理器,并使用它来执行存储库方法。

Eloquent模型支持

如果我们使用 Ecotone 的聚合支持与 Eloquent 模型,我们可以使用特殊类型的业务接口——存储库。

假设 Person 是我们的 Eloquent 模型,那么我们可以像这样定义存储库接口:

<?php

interface PersonRepository
{
    #[Repository]
    public function get(int $personId): ?Person;

    #[Repository]
    public function save(Person $person): void;
}

Repository 注解告诉 Ecotone 该接口是存储库。Ecotone 将使用 getRepository() 方法找到相关的 Eloquent 模型类,并使用它来执行存储库方法。

Ecotone 的数据库抽象可以帮助我们提高数据库访问的生产力和可维护性。通过使用简单的接口来访问数据库,我们可以专注于业务逻辑,而无需担心底层的细节。

感兴趣的可以深入研究下Ecotone文档:https://docs.ecotone.tech/

上一篇 下一篇

相关阅读

发表评论

访客 访客
快捷回复: 表情:
评论列表 (有 0 条评论,139人围观)