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

admin 2024-01-24 658 阅读 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

发表评论

快捷回复: 表情:
aoman baiyan bishi bizui cahan ciya dabing daku deyi doge fadai fanu fendou ganga guzhang haixiu hanxiao zuohengheng zhuakuang zhouma zhemo zhayanjian zaijian yun youhengheng yiwen yinxian xu xieyanxiao xiaoku xiaojiujie xia wunai wozuimei weixiao weiqu tuosai tu touxiao tiaopi shui se saorao qiudale qinqin qiaoda piezui penxue nanguo liulei liuhan lenghan leiben kun kuaikule ku koubi kelian keai jingya jingxi jingkong jie huaixiao haqian aini OK qiang quantou shengli woshou gouyin baoquan aixin bangbangtang xiaoyanger xigua hexie pijiu lanqiu juhua hecai haobang caidao baojin chi dan kulou shuai shouqiang yangtuo youling
提交
评论列表 (有 0 条评论, 658人围观)

最近发表

热门文章

最新留言

热门推荐

标签列表