Skip to content

14. 应用练习 – 第6版

Image

我们刚刚实现了以下分层结构:

Image

示例中使用的 SGBD 实为 MySQL。 在“链接”一节中,我们注意到,在实现 [dao] 层的类中,没有任何内容表明我们使用了特定的 SGBD。 现在我们将通过使用另一个 SGBD(即 SGBD PostgreSQL)来验证这一点。分层架构如下:

Image

14.1. 安装 SGBD PostgreSQL

SGBD 和 PostgreSQL 的发行版可在 URL 和 [https://www.postgresql.org/download/](2019年5月)中获取。以下展示 64 位 Windows 版本的安装过程:

Image

Image

  • [1-4] 中,下载 SGBD 的安装程序;

运行下载的安装程序:

Image

  • [6] 中,指定安装目录;

Image

  • [8] 步骤中,选项 [Stack Builder] 对于此处的操作没有用;
  • [10] 中,保留系统提示的默认值;

Image

  • [12-13] 中,此处已设置密码 [root]。 这将是名为 [postgres] 的 SGBD 管理员的密码。PostgreSQL 也称其为超级用户;
  • [15] 处,保留默认值:这是 SGBD 的监听端口;

Image

  • [17] 中,保留默认值;
  • [19] 中,显示安装配置摘要;

Image

Image

在 Windows 系统中,SGBD 和 PostgreSQL 被安装为自动启动的 Windows 服务。大多数情况下,这并非理想状态。 我们将修改此配置。在 Windows 搜索栏中输入 [services] [24-26]

Image

  • 为 [29],可以看到 SGBD 和 PostgreSQL 服务处于自动模式。通过访问 [30] 服务的属性来更改此设置:

Image

  • [31-32] 中,将启动方式设为手动;
  • 对于 [33],停止该服务;

当您需要手动启动 SGBD 时,请返回 [services] 应用程序,右键单击 [postgresql] 服务 (34) 并启动它 (35)。

14.2. 启用 PDO 扩展(隶属于 SGBD 和 PostgreSQL)

我们将修改配置 PHP 的文件 [php.ini](参见链接段落):

Image

  • 将其修改为 [2],并确认 PostgreSQL 的扩展 PDO 已启用。完成上述操作后,保存修改并重新启动 Laragon,以确保修改生效。 随后请直接在 Laragon 中检查 PHP 的配置。

14.3. 使用工具 [pgAdmin] 管理 PostgreSQL

启动 SGBD 的 Windows 服务 PostgreSQL(参见链接段落)。然后,与启动 [services] 工具的方式相同, 启动 [pgadmin] 工具,该工具用于管理 SGBD、PostgreSQL 和 [1-3]

Image

在某些情况下,系统可能会要求您输入超级用户的密码。该密码名为 [postgres]。 您在安装 SGBD 时已设置了该密码。在本文档中,我们在安装过程中为超级用户设置的密码是 [root]

  • [4] 中,[pgAdmin] 是一个 Web 应用程序;
  • [5] 中,[pgAdmin] 检测到的 PostgreSQL 服务器列表,此处为 1;
  • [6] 中,是我们启动的 PostgreSQL 服务器;
  • [7] 中,SGBD 的数据库,此处为 1;
  • [8] 中,[postgresql] 数据库由超级用户 [postgres] 管理;

首先创建用户 [admimpots],密码为 [mdpimpots]

Image

Image

  • [17] 中,我们设置了 [mdpimpots]

Image

  • [21] 中,工具 [pgAdmin] 将向 SGBD PostgreSQL 输出代码 SQL。 这是学习 PostgreSQL 专有语言 SQL 的一种方式;
  • 在[22]中,经过[Save]向导的验证后,已创建用户[admimpots];

现在我们创建数据库 [dbimpots-2019]

Image

右键单击 [23],然后单击 [24-25] 以创建新的数据库。 在 [26] 选项卡中,将数据库名称定义为 [27],所有者定义为 [admimpots] [28]

Image

  • [30] 中,数据库创建代码 SQL;
  • [31] 中,经过向导 [Save] 的验证后,创建了数据库 [dbimpots-2019]

现在,我们将创建包含列 [id, limites, coeffr, coeffn] 的表 [tbtranches]。 PostgreSQL的一个特点是列名区分大小写(大写/小写),而其他SGBD通常不区分大小写。 因此,对于 MySQL,即使表 [tbtranches] 的实际列为 [LIMITES, COEFFR, COEFFN],命令 [select limites, coeffR, coeffN from tbtranches] 仍可正常运行。 对于 PostgreSQL,命令 SQL 将无法运行。 此时虽然可以写成 [select LIMITES, COEFFR, COEFFN from tbtranches],但仍然无法运行,因为 PostgreSQL 会执行 [select limites, coeffr, coeffn from tbtranches] 命令:它默认将列名转换为小写。 要避免这种情况,需写成:[select "LIMITES", "COEFFR", "COEFFN" from tbtranches],即用引号将列名进行保护。基于这些原因,我们将为列命名使用小写字母。 数据库对象的名称可能导致 SGBD 之间出现不兼容,因为某些名称在某些 SGBD 中是保留字,而在其他 SGBD 中则不是。

我们创建表 [tbtranches]

Image

  • 使用 [40] 按钮创建列;

Image

Image

  • 通过 [Save] 完成创建向导后,将生成表 [tbtranches] [52-53]

我们需要告知 SGBD,在向表中插入一行时,它应自行生成主键 [id]

Image

  • [56] 中,访问主键 [id] 的属性;
  • [59] 中,需指定该列的类型为 [Identity]。这将导致 SGBD 生成主键的值;

Image

  • [62] 中,为该操作生成的代码为 SQL;

[tbtranches]表现已准备就绪。

我们重复相同的操作来创建表 [tbconstantes]。我们给出要获得的结果:

Image

Image

Image

数据库 [dbimpots-2019] 现已准备就绪。我们将向其中填充数据。

如同之前对 MySQL 所做的那样,可以将数据库 [dbimpots-2019] 导出为文件 SQL。 如果数据库丢失或损坏,可以导入该SQL文件来重建数据库。在此我们仅导出数据库结构,而不导出其数据:

Image

Image

生成的文件如下:


--
-- PostgreSQL 数据库转储
--

-- 从数据库版本 11.2 导出
-- 由 pg_dump 版本 11.2 导出

-- 开始于 2019-07-04 08:20:31

SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;

SET default_tablespace = '';

SET default_with_oids = false;

--
-- TOC 条目 198(类 1259 OID 16408)
-- 名称:tbconstantes;类型:TABLE;模式:public;所有者:postgres
--

CREATE TABLE public.tbconstantes (
    plafond_qf_demi_part double precision NOT NULL,
    id integer NOT NULL,
    plafond_revenus_celibataire_pour_reduction double precision NOT NULL,
    plafond_revenus_couple_pour_reduction double precision NOT NULL,
    valeur_reduc_demi_part double precision NOT NULL,
    plafond_decote_celibataire double precision NOT NULL,
    plafond_decote_couple double precision NOT NULL,
    plafond_impot_celibataire_pour_decote double precision NOT NULL,
    plafond_impot_couple_pour_decote double precision NOT NULL,
    abattement_dix_pourcent_max double precision NOT NULL,
    abattement_dix_pourcent_min double precision NOT NULL
);


ALTER TABLE public.tbconstantes OWNER TO postgres;

--
-- TOC 条目 199(类 1259 OID 16411)
-- 名称:tbconstantes_id_seq;类型:SEQUENCE;模式:public;所有者:postgres
--

ALTER TABLE public.tbconstantes ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
    SEQUENCE NAME public.tbconstantes_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1
);


--
-- TOC 条目 196(类 1259 OID 16399)
-- 名称:tbtranches;类型:TABLE;模式:public;所有者:admimpots
--

CREATE TABLE public.tbtranches (
    limites double precision NOT NULL,
    id integer NOT NULL,
    coeffr double precision NOT NULL,
    coeffn double precision NOT NULL
);


ALTER TABLE public.tbtranches OWNER TO admimpots;

--
-- TOC 条目 197(类 1259 OID 16404)
-- 名称:tbimpots_id_seq;类型:SEQUENCE;架构:public;所有者:admimpots
--

ALTER TABLE public.tbtranches ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
    SEQUENCE NAME public.tbimpots_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1
);


--
-- TOC 条目 2694(类 2606 OID 16429)
-- 名称:tbconstantes tbconstantes_pkey; 类型:CONSTRAINT; 模式:public; 所有者:postgres
--

ALTER TABLE ONLY public.tbconstantes
    ADD CONSTRAINT tbconstantes_pkey PRIMARY KEY (id);


--
-- TOC 条目 2692(类 2606 OID 16403)
-- 名称:tbtranches tbimpots_pkey; 类型:CONSTRAINT; 模式:public; 所有者:admimpots
--

ALTER TABLE ONLY public.tbtranches
    ADD CONSTRAINT tbimpots_pkey PRIMARY KEY (id);


--
-- TOC 条目 2821(类 0 OID 0)
-- 依赖项:198
-- 名称:TABLE tbconstantes; 类型:ACL; 模式:public; 所有者:postgres
--

GRANT ALL ON TABLE public.tbconstantes TO admimpots;


-- 完成于 2019-07-04 08:20:32

--
-- PostgreSQL 数据库转储完成
--

14.4. 填充表格 [tbtranches]

我们在“链接”段落中已经完成了 SGBD 和 MySQL 的相关工作。现在只需修改描述数据库的 [database.json] 文件即可:

Image

[database.json]文件修改后如下:


{
    "dsn": "pgsql:host=localhost;dbname=dbimpots-2019",
    "id": "admimpots",
    "pwd": "mdpimpots",
    "tableTranches": "public.tbtranches",
    "colLimites": "limites",
    "colCoeffR": "coeffr",
    "colCoeffN": "coeffn",
    "tableConstantes": "public.tbconstantes",
    "colPlafondQfDemiPart": "plafond_qf_demi_part",
    "colPlafondRevenusCelibatairePourReduction": "plafond_revenus_celibataire_pour_reduction",
    "colPlafondRevenusCouplePourReduction": "plafond_revenus_couple_pour_reduction",
    "colValeurReducDemiPart": "valeur_reduc_demi_part",
    "colPlafondDecoteCelibataire": "plafond_decote_celibataire",
    "colPlafondDecoteCouple": "plafond_decote_couple",
    "colPlafondImpotCelibatairePourDecote": "plafond_impot_celibataire_pour_decote",
    "colPlafondImpotCouplePourDecote": "plafond_impot_couple_pour_decote",
    "colAbattementDixPourcentMax": "abattement_dix_pourcent_max",
    "colAbattementDixPourcentMin": "abattement_dix_pourcent_min"
}
  • 第2行:DSN已更改,[pgsql]表示当前处理的是Postgres的SGBD;
  • 第 5 行和第 9 行:在表名前添加了其所属模式的名称 [public]。这并非绝对必要,因为当表名中未指定模式时,默认使用的模式是 [public]
  • 第 6-8 行、第 10-19 行:列名已更改;

用于填充数据库 [dbimpots-2019] 的脚本 [MainTransferAdminDataFromJsonFile2PostgresDatabase.php] 如下:


<?php

// 严格遵守函数参数的声明类型
declare (strict_types=1);

// 命名空间
namespace Application;

// PHP 错误处理
// ini_set("display_errors", "0");
// 接口和类的引入
require_once __DIR__ . "/../../version-05/Entities/BaseEntity.php";
require_once __DIR__ . "/../../version-05/Entities/TaxAdminData.php";
require_once __DIR__ . "/../../version-05/Entities/TaxPayerData.php";
require_once __DIR__ . "/../../version-05/Entities/Database.php";
require_once __DIR__ . "/../../version-05/Entities/ExceptionImpots.php";
require_once __DIR__ . "/../../version-05/Utilities/Utilitaires.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao.php";
require_once __DIR__ . "/../../version-05/Dao/TraitDao.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../../version-05/Dao/DaoTransferAdminDataFromJsonFile2Database.php";
//
// 常量定义
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";

//
try {
  // 创建 [dao] 层
  $dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
  // 将数据传输到数据库
  $dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
  // 显示错误
  print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// 结束
print "Terminé\n";
exit;

注释

仅第12-21行发生了变化,这些行负责加载应用程序运行所需的文件。之所以发生变化,是因为值[__DIR__]发生了变化:它现在指向文件夹[version-07/Main]

执行此脚本后,在表 [tbtranches] 中将得到以下结果:

Image

  • 右键单击 [1],然后单击 [2-3]
  • [4] 中,确实包含了税率区间的数据;

对常量表 [tbconstantes] 重复上述操作:

Image

Image

Image

需要注意的是,在执行脚本时,Laragon 应用程序无需处于活动状态:既不需要 Apache 服务器,也不需要 SGBD 和 MySQL。 只需启动了 Windows 服务的 SGBD PostgreSQL 即可。

14.5. 税款计算

Image

[dao] (3) 和 [métier] (2) 层已编写完成。我们在“链接”段落中已编写了 SGBD MySQL 的主脚本。 我们只需复用 [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase.php] 脚本,并将其适配至 SGBD 和 PostgreSQL。该脚本现更名为 [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.php]

Image

[MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.php]脚本如下:


<?php

// 严格遵守函数参数的声明类型
declare (strict_types=1);

// 命名空间
namespace Application;

// 由 PHP 处理错误
//ini_set("display_errors", "0");
// 接口和类的引入
require_once __DIR__ . "/../../version-05/Entities/BaseEntity.php";
require_once __DIR__ . "/../../version-05/Entities/TaxAdminData.php";
require_once __DIR__ . "/../../version-05/Entities/TaxPayerData.php";
require_once __DIR__ . "/../../version-05/Entities/Database.php";
require_once __DIR__ . "/../../version-05/Entities/ExceptionImpots.php";
require_once __DIR__ . "/../../version-05/Utilities/Utilitaires.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao.php";
require_once __DIR__ . "/../../version-05/Dao/TraitDao.php";
require_once __DIR__ . "/../../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once __DIR__ . "/../../version-05/Métier/InterfaceMetier.php";
require_once __DIR__ . "/../../version-05/Métier/Metier.php";
//
// 常量定义
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
const RESULTS_FILENAME = "../Data/resultats.json";
const ERRORS_FILENAME = "../Data/errors.json";
const TAXPAYERSDATA_FILENAME = "../Data/taxpayersdata.json";

try {
  // 创建 [dao] 层
  $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
  // 创建 [métier] 层
  $métier = new Metier($dao);
  // 批处理模式下的税款计算
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // 显示错误
  print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// 结束
print "Terminé\n";
exit;

注释

仅第12至22行发生了变化,这些行负责加载应用程序运行所需的文件。之所以发生变化,是因为[__DIR__]的值发生了变化:它现在指向文件夹[version-07/Main]

运行结果

与之前版本中获得的结果相同。

14.6. [Codeception] 测试

与之前版本一样,我们通过 [Codeception] 测试来验证此版本:

Image

14.6.1. [dao]层测试

[DaoTest.php]的测试内容如下:


<?php

// 严格遵守函数参数的声明类型
declare (strict_types=1);

// 命名空间
namespace Application;

// 根目录
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-06");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");

// 接口和类的包含
require_once ROOT . "/../version-05/Entities/BaseEntity.php";
require_once ROOT . "/../version-05/Entities/TaxAdminData.php";
require_once ROOT . "/../version-05/Entities/TaxPayerData.php";
require_once ROOT . "/../version-05/Entities/Database.php";
require_once ROOT . "/../version-05/Entities/ExceptionImpots.php";
require_once ROOT . "/../version-05/Utilities/Utilitaires.php";
require_once ROOT . "/../version-05/Dao/InterfaceDao.php";
require_once ROOT . "/../version-05/Dao/TraitDao.php";
require_once ROOT . "/../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";

// 第三方库
require_once VENDOR . "/autoload.php";

// 常量定义
const DATABASE_CONFIG_FILENAME = ROOT ."../Data/database.json";

class DaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    parent::__construct();
    // 创建 [dao] 层
    $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // 测试
  public function testTaxAdminData() {

  }

}

注释

  • 第 9-28 行:定义测试环境。我们使用与链接段落中描述的主脚本 [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] 相同的环境,但不包含 [métier] 层;
  • 第 34-39 行:构建 [dao] 层;
  • 第38行:[$this→taxAdminData]属性包含待测试的数据;
  • 第42-44行:方法[testTaxAdminData]即链接段落中所述的方法;

测试结果如下:

Image

14.6.2. [métier] 层测试

[MetierTest.php] 的测试如下:


<?php

// 严格遵守函数参数的声明类型
declare (strict_types=1);

// 命名空间
namespace Application;

// 根目录
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-06");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");

// 接口和类的包含
require_once ROOT . "/../version-05/Entities/BaseEntity.php";
require_once ROOT . "/../version-05/Entities/TaxAdminData.php";
require_once ROOT . "/../version-05/Entities/TaxPayerData.php";
require_once ROOT . "/../version-05/Entities/Database.php";
require_once ROOT . "/../version-05/Entities/ExceptionImpots.php";
require_once ROOT . "/../version-05/Utilities/Utilitaires.php";
require_once ROOT . "/../version-05/Dao/InterfaceDao.php";
require_once ROOT . "/../version-05/Dao/TraitDao.php";
require_once ROOT . "/../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/../version-05/Métier/InterfaceMetier.php";
require_once ROOT . "/../version-05/Métier/Metier.php";
// 第三方库
require_once VENDOR . "/autoload.php";
// 常量定义
const DATABASE_CONFIG_FILENAME = ROOT . "../Data/database.json";

class MetierTest extends \Codeception\Test\Unit {
  // 业务层
  private $métier;

  public function __construct() {
    parent::__construct();
    // 创建该层[dao]
    $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
    // 创建图层 [métier]
    $this->métier = new Metier($dao);
  }

  // 测试
  public function test1() {

  }
--------------------------------------------------------------------
  public function test11() {

  }

}

注释

  • 第 9-28 行:定义测试环境。我们使用与链接段落中描述的主脚本 [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] 相同的环境;
  • 第34-40:构建[dao]和[métier]层;
  • 第 39 行:[$this→métier] 属性引用 [métier] 图层
  • 第43-49行:[test1, test2…, test11]方法链接段落中所述的方法;

测试结果如下:

Image