atk4/data

敏捷数据 - 数据库访问抽象框架

5.2.0 2024-06-15 14:41 UTC

README

敏捷工具包 是一个用 PHP 编写的低代码框架。敏捷 UI 实现了服务器端渲染引擎和超过 50 个 UI 通用组件,用于与您的数据模型交互。

敏捷数据是一个用于定义您的“业务层”的框架,该层与您的“表示层”和“持久化”分离。与 敏捷 UI 一起,您可以使用“开箱即用”的用户界面或使用 敏捷 API - 通用 API 端点。

  • 敏捷数据使用 PHP 定义您的业务对象、它们的属性和操作。
  • 敏捷数据与 SQL、NoSQL 或外部 API 源一起工作。
  • 敏捷数据通过最少代码插入到通用 UI 组件(Crud、Card、Table、Form 等)中。
  • 敏捷数据支持“用户操作”。UI 层使用“操作执行器”读取 ACL 控制的元数据。
  • 敏捷数据对开发者友好且易于学习
  • 敏捷数据性能高,能够通过高级表达式将聚合逻辑抽象化并将其转移到强大的数据库持久化(如 SQL)中
  • 敏捷数据是可扩展的 - 字段类型、持久化类型、关系和操作类型可以扩展。

Build CodeCov GitHub release Code Climate

快速链接: 文档 | Discord 通道 | ATK UI

ATK 数据与 ORM 是否相似?

是的,也不是。

敏捷数据是一个数据持久化框架 - 类似于 ORM,它帮助您避开原始 SQL。与 ORM 不同,它将对象映射到“数据集”而不是“数据记录”。操作数据集提供了更高层次的抽象。

$vipClientModel = (new Client($db))->addCondition('is_vip', true);

// express total for all VIP client invoices. The value of the variable is an object
$totalDueModel = $vipClientModel->ref('Invoice')->action('fx', ['sum', 'total']);

// single database query is executed here, but not before!
echo $totalDueModel->getOne();

在其他 ORM 中,类似的实现可能会是 缓慢、笨拙、有限或存在缺陷

如何将 ATK 数据集成到 UI(或 API)中

敏捷工具包是一个低代码框架。一旦您已定义了您的业务对象,它就可以与 UI 小部件

Crud::addTo($app)->setModel(new Client($db), ['name', 'surname'], ['edit', 'archive']);

或与 API 端点关联

$api->rest('/clients', new Client($db));

可扩展性和插件

ATK 数据是可扩展的,并提供了一系列插件,如 Audit

无论您的模型如何构建以及使用的数据库后端是什么,它都可以轻松地与任何第三方插件一起使用,如 Charts

使用 ATK 数据的好处

为中等到大型 PHP 应用程序和框架设计,ATK 数据是数据映射器的清晰实现,将

  • 使您的应用程序真正实现数据库无关性。SQL?NoSQL?RestAPI?缓存?使用这些中的任何一种,而无需重构您的代码来加载和存储数据。
  • 在服务器上执行更多操作。敏捷数据将查询逻辑转换为特定于服务器的语言(例如 SQL),然后通过单个语句为您提供您需要的确切数据行/列,无论其多么复杂。
  • 数据架构透明度。随着您的数据库结构发生变化,您的应用程序代码无需重构。用表达式替换字段,对数据进行非规范化/规范化,连接和合并表。只需在单个位置更新您的应用程序。
  • 扩展。《Audit》- 通过“撤销”功能透明记录所有编辑、更新和删除操作。
  • 开箱即用的UI。谁还想在今天构建管理系统呢?有数十个专业组件: Crud Grid Form 以及像 Charts 这样的附加组件,只需3行代码即可添加到您的PHP应用程序中。
  • 敏捷数据(Agile Data)的RestAPI服务器目前正在开发中。
  • 敏捷数据及其上述所有扩展均采用MIT许可证,可免费使用。

自从2016年首次介绍敏捷数据以来,我们的早期采用者已经在大型生产PHP项目中使用它。现在是您尝试敏捷数据的时候了。

入门指南

观看 快速入门屏幕录制。还有 完整文档 (PDF)。

ATK数据依赖于ATK核心,并且可以通过ATK UI得到极大的补充。

  • 敏捷核心 - 记录各种低级特性,例如容器、钩子或异常(《PDF》)
  • 敏捷UI - 记录可选UI组件以及如何使用它们构建Web应用程序(《PDF》)

何时使用敏捷数据?

我们相信,作为一名开发者,您应该高效地利用时间。处理大量数据不是高效的方式。敏捷数据使UI组件、导出器、导入器或RestAPI服务器能够以通用的方式进行实现。

HTML应用程序和管理系统

大多数ORM(包括您现在可能正在使用的ORM)都存在一个缺点。作为一个框架,它们没有足够的信息来描述您的数据模型中的模型、字段、关系和条件。

integration-orm

结果,UI层无法简单地发现您的发票如何与客户相关联。这使得您需要编写大量的粘合代码 - 执行查询并将数据输入到UI层。

在大多数ORM中,您不能设计通用的Crud或Form,以便与任何模型一起工作。结果,在客户端框架的面前,服务器端渲染变得日益过时。

敏捷数据解决了这种平衡。对于表示逻辑,您可以使用像 敏捷UI 这样的工具,它包含通用的Crud、Form实现或其他模块,这些模块接受敏捷数据的模型协议。

$presentation->setModel($businessModel);

现在,这种平衡重新调整,使得实现任何通用UI组件成为可能,这些组件将与您的自定义数据模型和自定义持久化(数据库)一起工作。

integration-atk

需要注意的是,粘合剂还可以与模型交互,为特定的用例准备模型。

$grid = new \Atk4\Ui\Table();
$data = new Order($db);
$data->addCondition('is_new', true);
$data->addCondition('client_id', $_GET['client_id']);
$grid->setModel($data);

$html = $grid->render();

领域模型报告

面向对象的方法旨在隐藏实现的复杂性。然而,每次您需要包含聚合或连接的报表数据时,您都必须深入了解您的数据库结构,以提取一些奇特的ORM技巧或注入自定义SQL查询。

敏捷数据被设计成您的所有代码都只能依赖于模型对象。这包括报表。

以下示例通过仅依赖模型逻辑构建复杂的“工作盈利报告”。

class JobReport extends Job
{
    protected function init(): void
    {
        parent::init();

        // Invoice contains Lines that may relevant to this job
        $invoice = new Invoice($this->getPersistence());

        // we need to ignore draft invoices
        $invoice->addCondition('status', '!=', 'draft');

        // build relation between job and invoice line
        $this->hasMany('InvoiceLines', ['model' => static function () use ($invoice) {
            return $invoice->ref('Lines');
        }])
            ->addField('invoiced', [
                'aggregate' => 'sum',
                'field' => 'total',
                'type' => 'atk4_money'
            ]);

        // build relation between Job and Timesheets
        $this->hasMany('Timesheets', ['model' => static function (Persistence $persistence) {
            // next we need to see how much is reported through timesheets
            $timesheet = new Timesheet($persistence);

            // timesheet relates to client, import client.hourly_rate as expression
            $timesheet->getReference('client_id')->addField('hourly_rate');

            // calculate timesheet cost expression
            $timesheet->addExpression('cost', ['expr' => '[hours] * [hourly_rate]']);

            return $timesheet;
        }])
            ->addField('reported', [
                'aggregate' => 'sum',
                'field' => 'cost',
                'type' => 'atk4_money'
            ]);

        // finally lets calculate profit
        $this->addExpression('profit', ['expr' => '[invoiced] - [reported]']);

        // profit margin could be also useful
        $this->addExpression('profit_margin', ['expr' => 'coalesce([profit] / [invoiced], 0)']);
    }
}

您的报表模型

  • 将查询逻辑移动到数据库(SQL)中
  • 仍然是一个模型,因此与所有UI组件和扩展兼容

为了在HTML表格中输出结果

$grid = new \Atk4\Ui\Grid();
$data = new JobReport($db);
$grid->setModel($data);

$html = $grid->render();

或者,如果你想使用https://github.com/atk4/chart以图表形式显示

$chart = new \Atk4\Chart\BarChart();
$data = new JobReport($db);

// BarChart wants aggregated data
$data->addExpression('month', ['expr' => 'month([date])']);
$aggregate = new AggregateModel($data);
$aggregate->setGroupBy(['month'], [
    'profit_margin' => ['expr' => 'sum'],
]);

// associate presentation with data
$chart->setModel($aggregate, ['month', 'profit_margin']);
$html = $chart->html();

在这两种情况下,你只需要执行一个单一的SQL查询。

大型应用程序和企业使用

重构

敏捷数据的一个最佳好处是能够以不会影响整个应用程序的方式重构数据库结构。这极大地简化了你的Q/A周期并降低了应用程序维护成本。以下是一个示例场景

现有应用程序根据SQL公式计算利润,但由于数据量巨大,计算缓慢。解决方案是添加一个“利润”字段,其值将自动更新。

敏捷数据为你提供所有工具,只需几步即可完成此操作

  • 通过将“表达式”替换为常规字段来更新模型定义。
  • 创建一个“迁移器”脚本,使用动作计算表达式。
  • 通过添加模型钩子(afterSave)来更改模型行为,在同一个ACID事务中重新计算“利润”。

这不会破坏你应用程序的其他部分——UI、RestAPI或报告将继续工作,但速度更快。

审计和定制

我在视频中解释了一些基本的定制方法:https://www.youtube.com/watch?v=s0Vh_WWtfEs&index=5&list=PLUUKFD-IBZWaaN_CnQuSP0iwWeHJxPXKS

文档中还有一个“高级主题”部分:https://atk4-data.readthedocs.io/en/develop/advanced.html

多系统应用程序

大多数SaaS系统都有用户数据可能无法被其他用户访问的要求。然而,数据存储在同一个数据库中,并且仅通过“system_id”或“user_id”字段区分。

敏捷数据有一个使用模式,将自动在这些模型上根据这些条件限制访问。这将确保当前登录用户无法添加任何不属于他的数据或访问任何数据,即使开发者代码中有错误。

从一个数据库迁移到另一个数据库以及跨持久性

使用敏捷数据,你可以无缝地将数据从一种持久性移动到另一种持久性。如果你依赖于一些新持久性不支持的功能(例如,表达式),你可以通过在应用程序服务器上执行的回调计算来替换它们。

通常——你的应用程序的其他部分不会受到影响,你甚至可以使用多种不同类型的持久性并仍然导航引用。

支持

因为我们的团队实现了敏捷数据,我们培训了专家,他们可以提供商业咨询、培训和支持。使用我们的联系表单进行查询:https://www.agiletoolkit.org/contact

框架集成

敏捷数据(在某些情况下是敏捷UI)已被社区与其他流行框架集成

Q&A

问:我注意到敏捷数据使用子查询而不是JOIN。我认为JOIN更高效。

虽然在大多数情况下,现代SQL子查询的速度与JOIN相当,但敏捷数据的SQL持久性也实现了“JOIN”支持。默认情况下,子查询的使用更安全,因为它可以在相关实体上隐含条件。

但是,你可以通过JOIN导入字段

问:我不喜欢使用$book->set('field', 123),我更喜欢属性。

敏捷模型不是实体。它们不代表一条记录,而是一组记录。这就是为什么模型有一些重要的属性:$model->getId()$model->getPersistence()model->getDataRef()

更多关于如何在处理单个数据记录的信息。

问:我不喜欢将类 \Atk4\Data\Model 作为父类使用

Model 实现了许多基本功能。如果您需要更深入的说明,请阅读我的博客文章:https://www.agiletoolkit.org/blog/why-should-you-extend-your-entity-class

问:敏捷数据有较小的社区

这是您可以改变的事情。如果您查看敏捷数据的特性,并认为它值得更多关注,请通过传播信息并扩大我们的社区来帮助我们。

敏捷数据仍然是一个相对较新的框架,直到PHP社区认识到它需要时间。

问:有断链/文档/页面

我们专注于制作高质量的软件并将其免费提供给用户。我们将尽力解决任何断链/页面或过时页面,但我们的资源有限。

问:是否有敏捷数据/敏捷UI的培训材料?

我们正在制作。目前,请访问我们的Discord

问:我怎样才能帮忙/贡献?

打个招呼。我们喜欢结识新朋友,无论他们对PHP和框架有多熟悉 Discord

如果您想帮忙,我们在问题系统中有一个特殊的标签 Help Wanted

以下部分信息可能已过时,需要清理。

敏捷数据概览

敏捷数据以实用、易于学习和使用的方式实现了多种高级数据库访问模式,如Active Record、持久化映射、领域模型、事件源、操作、钩子、数据集和查询构建,适用于任何具有SQL或NoSQL数据库的框架,并满足所有企业级特定要求。

在调用查询之前,您可以先操作对象。下面的代码片段将用于您的现有客户、订单和订单行数据库,并查询所有VIP客户所下订单的总金额。查看生成的查询,您会发现一个实现细节——行总额不是物理存储在数据库中,而是以价格和数量的乘积表示。

$m = new Client($db);
echo $m->addCondition('vip', true)
    ->ref('Order')->ref('Line')->action('fx', ['sum', 'total'])->getOne();

生成的查询将始终使用参数变量,如果供应商驱动程序支持它们(如PDO)。

select sum(`price`*`qty`) from `order_line` `O_L` where `order_id` in (
    select `id` from `order` `O` where `client_id` in (
        select `id` from `client` where `vip` = :a
    )
)

// :a is "Y"

敏捷数据不仅适用于SQL数据库。它可以用于解码表单提交数据($_POST)或甚至与自定义RestAPI一起工作。"审计跟踪"、"访问控制"和"软删除"等功能的零配置实现以及"撤销"、"全局作用域"和"跨持久性"等新功能,使您的敏捷数据代码无需额外配置即可适用于企业级。

上述所有内容都不会增加您的业务逻辑代码的复杂性。您不需要创建XML、YAML文件或注释。也没有强制性的缓存。

我的下一个示例演示了当您存储新的订单数据时,代码看起来有多简单和干净。

$m = new Client($db);
$m->loadBy('name', 'Pear Company');
$m->ref('Order')
    ->save(['ref' => 'TBL1', 'delivery' => new DateTime('+1 month')])
    ->ref('Lines')->import([
        ['Table', 'category' => 'furniture', 'qty' => 2, 'price' => 10.5],
        ['Chair', 'category' => 'furniture', 'qty' => 10, 'price' => 3.25],
    ]);

生成的查询(为了可读性,我移除了反引号和参数化变量)使用了简洁的语法,并展示了一些“幕后”逻辑。

  • 新订单必须属于公司。同时,公司不能是软删除状态。
  • delivery 存储在字段 delivery_date 中,同时 DateTime 类型被映射为 SQL 友好的日期格式。
  • order_id 将自动与行一起使用。
  • category_id 可以在 INSERT 语句中直接查找(这是 SQL 引用字段的常规功能)。
select id, name from client where name = 'Pear Company' and is_deleted = 0;
insert into order (company_id, ref, delivery_date)
    values (293, 'TBL1', '2015-18-12');
insert into order_lines (order_id, title, category_id, qty, price) values
    (201, 'Table', (select id from category where name = 'furniture'), 2, 10.5),
    (201, 'Chair', (select id from category where name = 'furniture'), 19, 3.25);

如果你喜欢这些例子,并想亲自尝试,请继续访问 https://github.com/atk4/data-primer

介绍模型

Agile Data 使用独立于供应商且轻量级的 Model 类来描述您的业务实体。

class Client extends \Atk4\Data\Model
{
    public $table = 'client';

    protected function init(): void
    {
        parent::init();

        $this->addField('name');
        $this->addField('address');

        $this->hasMany('Project', ['model' => [Project::class]]);
    }
}

介绍动作

mapping

与模型(字段、条件、引用)相关的内容都是 PHP 内存中“领域模型”领域内的对象。当你调用 save() 时,框架会生成一个“动作”,该动作实际上会更新你的 SQL 表、调用 RestAPI 请求或将文件写入磁盘。

每个持久化实现动作的方式不同。SQL 可能是功能最全的一个。

GitHub release

介绍表达式

Agile Toolkit 中的智能字段表示为对象。由于继承,字段可以执行多种不同的操作。例如,SqlExpressionField 可以通过自定义 SQL 或 PHP 代码定义字段。

GitHub release

介绍引用

外键和关系是 RDBMS 的基本组成部分。虽然这在“持久化”中很有意义,但并非所有数据库都支持关系。

Agile Data 通过引入“引用”采取了不同的方法。它使您能够定义域模型之间的关系,这些关系可以与非关系数据库一起工作,同时允许您执行各种操作,如导入或聚合字段。(JOIN 的使用将在下面解释)。

GitHub release

模型条件和数据集

条件(或范围)是 ORM 中罕见且可选的功能,但它们是 Agile Data 中最显著的功能之一。它允许您创建表示多个数据库记录的对象,而实际上并不加载它们。

一旦定义了条件,它就会出现在动作中,同时也会限制您添加不符合条件的记录。

GitHub release

在领域模型内构建报告

在大多数框架中,当涉及到严重的数据聚合时,您必须做出选择——编写低效的领域模型代码或编写原始 SQL 查询。Agile Data 帮助您利用数据库的独特功能,同时让您保持在领域模型内。

我们如何在保持完全在领域模型内的同时,创建一个高效的查询来显示按客户国家分组的所有项目的总预算?在 Agile Data 中只需要一行代码。

GitHub release

你注意到查询自动排除了已取消的项目吗?

模型级连接

大多数 ORM 可以定义仅与单个 SQL 表一起工作的模型。如果您必须将逻辑实体数据存储在多个表中——很不幸,您将不得不自己进行一些链接。

敏捷数据允许您在模型中直接定义多个连接。当您调用join()另一个表时,您将能够从连接的表中导入字段。如果您创建了一个新的记录,数据将自动分配到表中,并且记录将正确地链接起来。

GitHub release

连接的最好之处在于,您可以为特定的查询将它们添加到现有的模型中。一些扩展甚至可以做到这一点。

深度模型遍历

敏捷数据最出色的功能之一是深度遍历。还记得您的ORM如何尝试实现各种多对多关系吗?在敏捷数据中,这不再是问题。

假设您想查看所有名称为两位的国家。来自位于两位名称国家的客户的有哪些项目?

敏捷数据可以通过查询或结果来回答。

GitHub release

高级功能和扩展

到目前为止,您所看到的示例只是您可以使用敏捷数据实现的可能性的一小部分。您现在有一个新的游乐场,您可以围绕强大的概念设计您的业务逻辑。

我们在敏捷数据中最看重的优点之一是能够抽象并在我们的坚实基础上添加更高级的功能。

可探索性

如果您将任何方法、插件或扩展中的$模型对象传递给,它们可以不仅发现数据,还可以发现字段类型和各种各样的元信息,其他模型的引用、支持的操作等等。

有了这个,创建一个自动包含允许值列表的下拉菜单的动态表单UI对象是可能的。

实际上,我们已经开始了敏捷UI项目的开发工作!

钩子

您现在有域级和持久化级钩子。使用域级钩子(afterLoad、beforeSave),您可以在操作前后操作字段数据。

另一方面,您可以使用持久化级钩子('beforeUpdateQuery'、'beforeSelectQuery'),并且您可以与强大的查询构建器交互,如果您需要,可以添加一些SQL选项(如insert ignore或calc_found_rows)。

而且,如果您将模型保存到NoSQL数据库,域级钩子将被执行,但SQL特定的钩子将不会执行。

扩展

大多数ORM将软删除、审计日志、时间戳等特性硬编码。在敏捷数据中,基础模型的实现非常轻量级,所有必要的功能都是通过外部对象添加的。

我们仍在开发我们的扩展库,但我们计划包括

  • 审计日志 - 记录模型中的所有操作(以及之前的字段值),提供可靠的撤销功能。
  • ACL - 基于登录用户的权限或自定义逻辑,灵活地限制对特定记录、字段或模型的访问。
  • 文件存储 - 允许您在模型中处理文件。实际上,文件存储在S3(或其他)中,但引用和元信息保留在数据库中。
  • 软删除、清除和恢复删除 - 几种策略、自定义字段、权限。

有关扩展的更多详细信息:https://www.agiletoolkit.org/data/extensions

性能

如果您想知道这些高级功能可能会如何影响加载数据和保存数据的性能,这里还有一个令人愉快的惊喜。加载数据、保存数据、迭代和删除记录不会创建新的内存对象。

foreach ($client->ref('Project') as $project) {
    echo $project->get('name') . "\n";
}

// $project refers to same object at all times, but $project's active data
// is re-populated on each iteration

不会预先加载不必要的内容。只会查询请求的列。行是流式传输的,我们永远不会试图将大量ID挤压到一个变量或查询中。

敏捷数据即使在数据库中有大量记录时也能快速高效地工作。

安全

当ORM承诺“安全性”时,它们并没有真正扩展到您希望执行子查询的情况。然后您必须处理原始查询组件并将它们自己拼接起来。

敏捷数据为表达式提供通用支持,每个表达式都支持转义参数。我的下一个示例将通过按国家长度过滤来添加作用域过滤。自动参数将确保任何恶意内容都会被正确转义。

$country->addCondition($country->expr('length([name]) = []', [$_GET['len']]));

生成的查询是

where length(`name`) = :a

当您尝试添加新国家时,将调用另一个出色的安全功能

$country->insert('Latvia');

此代码将失败,因为我们早期的条件“拉脱维亚”不满足。这使得其他各种用途变得安全

$client->load(3);
$client->ref('Order')->insert($_POST);

无论$_POST中有什么内容,新记录都将有client_id = 3

最后,以下也是可能的

$client->addCondition('is_vip');
$client->ref('Order')->insert($_POST);

无论POST数据的内容如何,只能为VIP客户创建订单。即使您执行多行操作,如action('select')action('fx'),它也只会应用于满足所有条件的记录。

这些安全措施是为了保护您免受人为错误的影响。我们认为输入清理仍然非常重要,您应该进行清理。

安装到现有项目

首先,通过composer安装敏捷数据

composer require atk4/data
composer g require psy/psysh:@stable # optional, but handy for debugging!

定义您的第一个模型类

namespace my;

class User extends \Atk4\Data\Model
{
    public $table = 'user';

    protected function init(): void
    {
        parent::init();

        $this->addField('email');
        $this->addField('name');
        $this->addField('password');

        // add your table fields here
    }
}

接下来创建console.php

<?php

require __DIR__ . '/vendor/autoload.php';

$db = \Atk4\Data\Persistence::connect(PDO_DSN, USER, PASS);
eval(\Psy\sh());

最后,运行console.php

$ php console.php

现在您可以探索了。尝试输入

> $m = new \my\User($db);
> $m->loadBy('email', 'example@example.com')
> $m->get()
> $m->export(['email', 'name'])
> $m->executeCountQuery()

敏捷核心和DSQL

敏捷数据依赖于DSQL - 查询构建器进行SQL持久性和多记录操作,通过操作进行。各种接口和PHP模式通过敏捷核心实现。

等等!为什么还需要另一个查询构建器?显然是因为现有的不够好。您可以在PHP中编写多供应商查询,从中获得更好的安全性、干净的语法并避免人为错误。

DSQL试图以不同的方式做事

  1. 可组合性。与其他库不同,我们递归地渲染查询,允许多层子查询。
  2. 占用空间小。我们不为所有供应商复制查询代码,而是使用巧妙的模板系统。
  3. 可扩展性。我们有三种不同的方式扩展DSQL以及第三方供应商驱动程序支持。
  4. 任何查询 - 任何复杂性的查询都可以通过DSQL表达。
  5. 几乎无依赖。在PHP应用程序或框架中使用DSQL。
  6. NoSQL支持。除了支持PDO外,DSQL还可以扩展以处理兼容SQL的NoSQL服务器。

DSQL简单而强大

$query = $connection->dsql();
$query->table('employees')
    ->where('birth_date', '1961-05-02')
    ->field('count(*)');
echo 'Employees born on May 2, 1961: ' . $query->getOne();

如果基本查询不有趣,更复杂的查询呢?

// establish a query looking for a maximum salary
$salary = $connection->dsql();

// create few expression objects
$eMs = $salary->expr('max(salary)');
$eDf = $salary->expr('TimeStampDiff(month, from_date, to_date)');

// configure our basic query
$salary
    ->table('salary')
    ->field(['emp_no', 'max_salary' => $eMs, 'months' => $eDf])
    ->group('emp_no')
    ->order('-max_salary')

// define sub-query for employee "id" with certain birth-date
$employees = $salary->dsql()
    ->table('employees')
    ->where('birth_date', '1961-05-02')
    ->field('emp_no');

// use sub-select to condition salaries
$salary->where('emp_no', $employees);

// join with another table for more data
$salary
    ->join('employees.emp_id')
    ->field('employees.first_name');

// finally, fetch result
foreach ($salary as $row) {
    echo 'Data: ' . json_encode($row) . "\n";
}

这将构建并执行一个查询,看起来像这样

SELECT
    `emp_no`,
    max(salary) `max_salary`,
    TimeStampDiff(month, from_date, to_date) `months`
FROM
    `salary`
JOIN
    `employees` on `employees`.`emp_id` = `salary`.`emp_id`
WHERE
    `salary`.`emp_no` in(select `id` from `employees` where `birth_date` = :a)
GROUP BY `emp_no`
ORDER BY max_salary desc

:a = "1961-05-02"

敏捷数据的UI

在一个有数百个不同的PHP Crud实现的宇宙中,我们认为您可能希望有一个开源的Grid/Crud/表单/其他UI库,专门为敏捷数据设计。

请考虑我们的其他MIT许可项目 - 敏捷UI来构建类似的东西

image

社区和支持

Stack Overflow Community Discord Community