使用@ ngrx / data和ActiveRecord从Angular访问RDB(附加)


1引言

本文是Angular#2 Advent Calendar 2019的第六天。

以下模块已更改为Rel9.x。
Angular,AngularMaterial,NgRx,flex-layout

我是Star Applications Co.,Ltd.的吉田文雄在谈论从Angular访问数据库时,似乎有许多主题,例如Firebase和GraphQL,但是最近有一种从NoSQL返回RDB的运动,因此我尝试了一个关系数据库。但是,从Angular编写SQL语句有点困难,因此我们将使用ActiveRecord。此外,在2019年夏天,当发布用于反应式编程的NgRx8.0时,还发布了一个名为@ ngrx / data的模块。通过使用此功能,您可以轻松创建一种模式,该模式允许数据沿一个方向流动。在本文中,我最初只考虑RDB的CRUD,但是由于我正在使用RDB,因此我尝试加入并进行事务。在本文的后面。

1.1 ActiveRecord

映射到面向对象类的数据库表通常称为OR映射器。 ActiveRecord是在众多OR映射器中的著名Ruby on Rails中实现的,而这次,出于以下原因,我们采用了ActiveRecord。

  • 我不想依赖特定的RDB
  • 它已经使用了相当长的时间,并且已经变得相当熟悉。
  • 使用迁移和固定装置之类的工具可以轻松完成数据库定义和数据创建。

1.2 @ ngrx / data

NgRx是受Redux启发的反应式编程,在Angular中也被称为事实上的标准。 NgRx很好,但是当数据类型很多时,例如,使用多种类型的表从数据库交换数据时(在业务应用程序中,有很多类似的情况),大量的Action,减速器,选择器必须经过编写和测试。这给程序员带来了沉重的负担。因此@ ngrx / data可用作外观,以减轻负担。我认为最好查看该领域的@ ngrx / data文档,而不是让我闲逛。另外,如果NgRx商店很困难并且您有点麻烦,为什么不尝试@ ngrx / data?表面上没有动作,派遣或减速器。只有直观易懂的CRUD命令。但是,由于它只是实体类型数据,因此将Store用于其他类型。

1.3概念图

本文中的概念图已发布。

qiita 画像.jpg

在此图中特别强调了红色箭头线。箭头线表示数据沿一个方向流动。存储数据为只读,并且始终通过发出命令来进行新的创建,更改和删除。
另外,Store是一种全局缓存,可以从每个Angular组件进行访问。由于@ ngrx / data处理从数据库到商店的数据累积以及将商店中的数据更新到数据库,因此Angular应用仅发出命令。
另外,上图只是一个概念图,实现形式是Angular应用程序中存在Store and Effect。

2示例程序

示例程序的位置如下。 https://github.com/YoshidaFumio/AngularRDB
若要运行示例程序,需要以下两个程序。

  • 码头工人
  • 码头工人组成

如果以上都不存在,请安装适用于Mac或Windows的Docker Desktop。请参考Docker的单独文档。

Angular源位于FrontEnd-sr??c目录下。也可以导入并运行源程序。有关详细信息,请参见README.md。

2.1样品内容

示例程序可以使用MySQL数据库中的四个表来引用和更新数据。初始数据被预先注册。
sampleprogTable.jpg

2.2样本执行

要执行示例,请按以下顺序执行。
1.在PC的相应目录中创建样本克隆
2. cd angularRDB
3. docker-compose build
4. docker-compose up -d
5.从浏览器中http://本地主机:4567 /
6.退出docker-compose down
如果要重新开始,请从上方4开始。数据库的内容将被重置。此外,可以使用以下docker-compose命令。
-输入ActiveRecord服务容器
Docker-compose exec activerecord-service / bin / bash
-输入数据库服务容器
Docker组成的exec数据库/ bin / bash

3数据库

ActiveRecord具有相当严格的命名约定,符合Ruby on Rails的设计哲学之一的" Convention over Configuration"政策。这次我们将遵循此。

3.1命名约定

数据库表,ActiveRecord模型类和@ ngrx /数据实体当前支持1:1:1。每个名称都有一个定义明确的名称,并且保留该名称将允许ActiveRecord @ ngrx / data工作。下表建议了分别用于雇员和分支机构的名称。

<表格>

项目名称

员工

分支


<身体>

数据库表名称

员工

分支

ActiveRecord模型名称

员工

分支

@ ngrx /数据实体名称

员工

分支

URL单数

员工

分支

URL多个系统

员工

分支

URL连接系统

员工加入

分支联接


在上表中,URL?是将数据从@ ngrx / data传递到Rest的BackEnd的ActiveRecord服务时的URL。

  • URL单一目标命令:getByKey,添加,更新,删除
  • URL多个系统目标命令:getWithQuery,getAll
  • URLJoin系统目标命令:getWithQuery

对于

表中的每个字段,将所有内容与数据库,模型类,实体匹配。
以下名称是保留名称,不能在表名称中使用。

  • 交易
  • 计算
  • 发现者

3.2表内容

让我们实际看一下表的结构。下图是使用MySQL命令创建表的示例。同样,它遵循ActiveRecord命名约定。

数据库.ddl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
create table employees (
  id int not NULL auto_increment PRIMARY KEY,
  branch_id int not NULL ,
  organization_id int ,
  position_id  int ,
  first_name varchar(64) default NULL,
  last_name varchar(64) default NULL,
  mobile_number varchar(128) default NULL,
  mail_address varchar(128) default NULL,
  twitter_link varchar(128) default NULL,
  birthday datetime default NULL ,
  entering_company datetime default NULL ,
  english_test int default 0 ,
  lock_version int default 0,
  created_at datetime not NULL,
  updated_at datetime not NULL,
  FOREIGN KEY(branch_id) REFERENCES branches(id),
  FOREIGN KEY(position_id) REFERENCES positions(id),
  FOREIGN KEY(organization_id) REFERENCES organizations(id)
)ENGINE=InnoDB CHARACTER SET utf8;

创建每个表时,始终需要以下四个字段。
1. ID号|字符串
2. lock_version号
3. created_at datetime
4. Updated_at datetime
其中的1、3和4是在使用Rails迁移等时自动设置的。总是在更新指令(PUT)时检查Lock_version,因此输入它。可以用另一个名称替换ID或合并多个字段,但是在设置@ ngrx / data时将对其进行处理。其他表的参考链接将基于ActiveRecord。在上图中,organization_id是指organizations表中的id。

4 ActiveRecord服务

ActiveRecord服务根据@Endgr上@ ngrx / data的请求将命令连接到数据库。来自数据库的回复数据将转换为JSON并返回到FrontEnd(@ ngrx / data,Angular)。可以这么说,它成为" JSON分发程序"。这次,我在Ruby和Sinatra中编写了这一部分。 "即使它是ActiveRecord也不是Rails吗?"我很可能会被责骂,但是Rails会感到很自在,但是如果您稍微出轨,可能会很痛苦,所以我选择了高度支持的Sinatra自由。

4.1型号定义

模型定义没有特别更改。这将是正常的ActiveRecord模式。

main.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#
#  ActiveRecord Models
#
class Branch < ActiveRecord::Base
  has_many :employees
end
class Organization < ActiveRecord::Base
  has_many :employees
end
class Position < ActiveRecord::Base
  has_many :employees
end
class Employee < ActiveRecord::Base
  belongs_to :branch
  belongs_to :position
  belongs_to :organization
end

有一个表,用于在模型定义的同时从请求的URL标识模型名称。
从第一列开始,是型号名称,单数系统,复数系统,Join系统。

main.rb

1
2
3
4
5
6
7
8
9
10
#  search table
#
#    [ModelName,single,plurial,joinsName]
#
MODEL_TABLE = [
  ['Branch' ,  'branch' , 'branches' , 'branchjoins'],
  ['Organization' ,  'organization' , 'organizations' , 'organizationjoins'],
  ['Position' ,  'position' , 'positions' , 'positionjoins'],
  ['Employee' , 'employee' , 'employees' , 'employeejoins'],
]

如果您想自己添加表,Ruby / Sinatra将修复以上两个地方。

4.2排序过程

Sinatra是Ruby的DSL,主要控制路由功能。让我们看看在Sinatra端生成并分发了什么样的请求。下面以一个名为Employee的实体为例。

<表格>

命令

URL

目录


<身体>

获取

?/员工/ 3

读取id为3的记录

获取

?/员工/

阅读所有员工

获取

?/员工/?其中?

其中满足以下条件的记录

发布

?/员工

创建一个新数据

放置

?/员工/ 4

更新ID为4的记录

删除

?/员工/ 5

删除id = 5的记录

获取

?/员工加入/?加入(?

执行联接

获取

?/计算/?其中?

查找最大值,最小值,平均值等

获取

?/查找/?其中?

检查是否符合条件

发布

?/ transaction /

事务处理


  • 员工部分为每个实体切换
  • 第一个"?/"部分是域等。示例http://本地主机:4567 / api / arreadwrite

5 @ ngrx / data的实践

最后是@ ngrx / data。从这里,我将解释如何使用Angular中的@ ngrx / data。

5.1设置

设置@ ngrx /数据并不那么困难。基本上,只需在实体单位中设置以下4个项目。

  • 其余接口根路径设置
  • 实体类别的定义
  • 目标实体的服务设置
  • 将实体信息设置为实体元数据

如果在NgModule中注册上述实体元数据,则@ ngrx / data可以自动使用。

5.1.1 Rest接口根路径

@ ngrx / data的默认路径是'/ api'。将此部分更改为" / api / arreadwrite"。要对其进行更改,请创建一个名为dataservice-config.ts的文件。

数据服务配置

1
2
3
4
5
6
7
import { DefaultDataServiceConfig } from '@ngrx/data';


export const defaultDataServiceConfig: DefaultDataServiceConfig = {
    root: 'api/arreadwrite',
    timeout: 3000, // request timeout
  }

此后,它在NgModule的提供程序中定义。

NgModule的一部分

1
2
3
4
import { defaultDataServiceConfig } from './dataservice-config';
        :
        :
  providers: [{ provide: DefaultDataServiceConfig,useValue: defaultDataServiceConfig}],

5.1.2类定义

为每个实体定义。如数据库表定义中所述,四个字段(id,lock_version,created_at,updated_at)是必需的。

组织

1
2
3
4
5
6
7
8
9
10
11
12
//
// Organization Class Define
//

export class Organization {
    id: number;
    org_code: string ;
    org_name: string ;
    lock_version: number ;
    created_at: string ;
    updated_at: string
}

5.1.3目标服务设置

这也为每个实体定义。

组织服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 1 //
 2 // Organization Service
 3 //
 4 // Facade of command(create , read , update , delete) and  selector$(entities)
 5 //
 6 import { Injectable } from '@angular/core';
 7 import { EntityCollectionServiceBase, EntityCollectionServiceElementsFactory }
   from '@ngrx/data';
 8 import { Organization } from './organization' ;

 9 @Injectable({ providedIn: 'root' })
10 export class OrganizationService extends EntityCollectionServiceBase<Organization> {
11  constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) {
12    super('Organization', serviceElementsFactory);
13  }
14 }

创建新的实体服务时,将上图中标记为"组织"的所有5个部分更改为不同的实体名称就足够了。将服务和类文件保留在第8行的2个位置,第10行的2个位置和第12行的1个位置上是一个好主意。我在路线上创建了服务,因此可以从任何地方访问它。我们在此处静态创建了服务,但是您也可以动态配置服务。有关详细信息,请参见entityCollectionServiceFactory。

5.1.4元数据注册

以下列表是示例程序的元数据注册。

实体元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 1 import { EntityMetadataMap, EntityDataModuleConfig } from '@ngrx/data';
 2 import { EntityAdapter , createEntityAdapter } from '@ngrx/entity';
 3 import { Organizationjoin } from './models/organizationjoin';
 4 import { SelectionModel } from '@angular/cdk/collections';
 5 import { IdSelector, Comparer } from '@ngrx/entity';
 6
 7 export function organizationjoinSelectid (organizationjoin:Organizationjoin)
 8  {  return organizationjoin.joinid }
 9
10 export function sortBranch(a: { branch_code: string }, b: { branch_code: string }): number {
11  return a.branch_code.localeCompare(b.branch_code);
12 }
13 export function sortOrganization(a: { org_code: string }, b: { org_code: string }): number {
14   return a.org_code.localeCompare(b.org_code);
15 }
16
17 export function sortPosition(a: { pos_code: string }, b: { pos_code: string }): 18 number {
19   return a.pos_code.localeCompare(b.pos_code);
20 }
21
22 const entityMetadata: EntityMetadataMap = {
23   Branch: {
24     sortComparer: sortBranch ,
25    entityDispatcherOptions : { optimisticDelete : false}
26  },
27   Organization: {
28     sortComparer: sortOrganization ,
29    entityDispatcherOptions : { optimisticDelete : false}
30  },
31  Position: {
32    sortComparer: sortPosition ,
33    entityDispatcherOptions : { optimisticDelete : false}
34  },
35  Employee: {
36    entityDispatcherOptions : { optimisticDelete : false}
37  },
38  Organizationjoin: {
39  //  selectId: (organizationjoin: Organizationjoin) => organizationjoin.joinid
40      selectId : organizationjoinSelectid
41  },
42  Calculation: {},
43  Finder: {} ,
44  Transaction: {}
45 };
46
47 const pluralNames = {
48  Branch: 'Branches'
49  };
50
51 export const entityConfig: EntityDataModuleConfig = {
52  entityMetadata,
53  pluralNames
54 };

上面列表的第22-45行将被注册为实体。如果什么也没有,那么一行就足够了。在每个实体的定义中,将sortComparer设置为指示每个实体的排序顺序。此外,entityDispatcherOptions会默认将其删除为悲观行为(在更新服务器后更新缓存)。除Delete之外的Add和Update是默认悲观的。第38行的组织加入是专用于加入的实体,并且主键已更改为加入id而不是id。第42-44行是唯一的实体,将按顺序进行说明。第47-49行是复数定义。如果未在此处定义,则复数将简单地添加一个'。第51-54行是整个元数据的注册。每个实体的详细设置几乎都在此位置进行。

接下来是NgModule的注册,实体元数据包含在entityConfig中。

app.module.ts

1
2
3
4
5
6
7
8
import { DefaultDataServiceConfig,EntityDataModule } from '@ngrx/data';
import { entityConfig } from './entity-metadata';
import { defaultDataServiceConfig } from './dataservice-config';
            :
imports[
            :
    EntityDataModule.forRoot(entityConfig)
]

5.2数据流

在解释数据流之前,让我们重新组织一下假设。 1.3另请参阅概念图。
前提
1.实体数据存储在一个名为Store 的大框架中
2. ReadOnly,无法直接写入存储数据
3.如果要更改存储中的数据,则必须发出命令
4.无法直接查看商店数据
5.要查看存储数据,请查看Observable变量。图1.3选择器
让我们看看该组件执行了哪种处理。

Organization-list.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 1 import { Component, OnInit , ChangeDetectionStrategy,
 2          ?SWITCH_RENDERER2_FACTORY__POST_R3__ } from '@angular/core';
 3 import { Observable } from 'rxjs';
 4 import { ActivatedRoute ,Router, NavigationExtras} from '@angular/router' ;
 5 import { Organization ,  OrganizationService  } from '../../models/organization';
 6
 7 @Component({
 8   selector: 'app-organization-list',
 9   templateUrl: './organization-list.component.html',
10   styleUrls: ['./organization-list.component.css'],
11   changeDetection : ChangeDetectionStrategy.OnPush
12 })
13 export class OrganizationListComponent implements OnInit {
14   organization$: Observable <Organization[]> ;
15
16   constructor(
17     private navroute : Router ,
18     private organizationService: OrganizationService,    
19     ) {
20        this.organization$ = organizationService.entities$ ;
21   }
22
23  ngOnInit() {
24  }

使用

1-5导入与实体相关的文件。第14行定义了可观察变量。以$结尾的变量是Observable变量,它们在第18行本地定义服务。接下来,让我们看一下模板方面。

Organization-list.component.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 1 <ng-container *ngIf = "organization$ | async as organizations">
 2     <mat-toolbar color="primary">
 3         <span class = "toolbar-top">部門一覧</span>
 4     </mat-toolbar>
 5     <div class="message-t">
 6         クリックすると詳細を表示して編集ができます。
 7     </div>
 8     <div class="outline">
 9         <div>
10             <span class="header-pos1">コード</span>
11             <span class="header-pos2">名前</span>
12         </div>            
13         <mat-nav-list>
14             <mat-list-item *ngFor="let organization of organizations"
15                 (click)="onSelect(organization.id)">
16                     <span>{{ organization.org_code }}</span>
17                     <span class="item-pos">{{ organization.org_name }}</span>
18             </mat-list-item>  
19         </mat-nav-list>
20         <div class="button-row">
21             <button mat-raised-button (click)="onClose()" class="button-m">閉じる</button>
22             <button mat-raised-button (click)="onNew()" class="button-m">新規</button>
23         </div>      
24     </div>
25 </ng-container>

第一行上的异步管道将数据带到组织$,同时将其扩展到组织。这种组织的多元化是实体本身的阵列。第14行的ngFor语句一个接一个地取出数组的元素,并将它们分配给称为organization的单个变量。

5.3 CRUD处理

@ ngrx /数据最初为一个实体准备了多种命令。参考:实体命令这是八个最重要的命令。

<表格>

命令

功能


<身体>

添加

添加实体

getByKey

从指定的ID导入一个数据

getAll

导入所有数据

加载

导入所有数据

getWithQuery

查看查询的内容并获取数据

更新

用指定的ID更新数据

删除

删除具有指定ID的数据

clearCache

删除存储区中的实体缓存数据


getAll和load之间的区别在于,如果存储中已经有数据,则getAll将合并,而load将替换它。

5.3.1创建处理

这是从样本中添加组织的示例。

Organization-newentry.component.ts

1
2
3
4
5
6
7
8
9
 1   onSave() {
 2     let orgdata = new Organization ;
 3     orgdata.org_code = this.organizationForm.value['code'] ;
 4     orgdata.org_name = this.organizationForm.value['name'] ;
 5     orgdata.lock_version = 0 ;
 6     this.organizationService.add(orgdata) ;
 7     let extra:NavigationExtras = { }
 8     this.navroute.navigate(['/organizationlist'],extra);
 9   }

与所有CRUD命令一样,不应将存储数据用作作为命令参数传递的数据。上面的第二行为参数创建一个数据区域。第3-4行设置表格数据。 lock_version的值最初为0。创建新的ID时,请保留其ID。在第7行,organizationService.add将发出命令。

5.3.2读取处理

共有三种模式用于读取处理。这是雇员(雇员实体)的示例。所有这些都从称为实体的employeeService的路由服务执行。 (请参阅5.1.3)

●读取特定ID

1
 employeeService.getByKey(4)

上面的示例读取id = 4的数据。

●阅读全部

1
 employeeService.getAll()

除了

getAll之外,还有一个名为load的命令。

●在特定条件下读取数据

1
2
3
4
 const QueryStr: string = `where("branch_id = ?")` ;
      ;
    queryNew = QueryStr.replace("?",3);
    this.employeeService.getWithQuery(queryNew) ;

读取满足branch_id = 3的条件的数据。条件表达式可以使用ActiveRecord格式,例如where语句。

5.3.3 UPDATE处理

这是一个从示例更新组织的示例。

Organization-detail.component.ts

1
2
3
4
5
6
 1  let orgdata = new Organization ;
 2  orgdata.id = this.curOrganization.id ;
 3  orgdata.org_code = this.organizationForm.value['code'] ;
 4  orgdata.org_name = this.organizationForm.value['name'] ;
 5  orgdata.lock_version = this.curOrganization.lock_version ;
 6  this.organizationService.update(orgdata) ;

在上面的第一行中为参数创建一个区域,并在第二行至第五行中设置数据。必须设置id和lock_version。在示例中,它是从商店中称为curOrganization的数据中获取的。其他数据将作为表单的输入数据。

5.3.4删除处理

1
 employeeService.delete(5)

上面的示例删除id = 5的数据。

5.4加入处理

好吧,即使您可以描述表之间的关系,仅靠CRUD还是不够的。因此,我使Join可用。首先,确定联接要获取的项目类型,然后创建一个具有与其匹配的数据结构的类。
首先,带上查询模板

Organization-join-list.component.ts

1
const QueryStr: string = `where("organizations.id = ?").joins(:employees).select('org_code,org_name,employees.*')` ;

在上图中?有一个符号,但这是稍后分配数值的地方。您应该在这里注意的是选择指令。查看select的内容,您将需要所有员工数据以及org_code,org_name字段。因此,使用这两个字段创建一个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//
// Organizationjoin Class Define
//

export class Organizationjoin {
    joinid: number ;
    org_code: string ;
    org_name: string ;
    id: number;
    branch_id: number;
    organization_id: number;
    position_id: number;
    first_name: string ;
    last_name: string ;
    mobile_number: string ;
    mail_address: string ;
    twitter_link: string ;
    birthday: string ;
    entering_company: string ;
    english_test: number ;
    lock_version: number ;
    created_at: string ;
    updated_at: string ;
    pos_code: string ;
    pos_name: string ;
}

嗯,这是一个添加了org_name和org_code的类,但是又添加了一个名为joinid的类。这是因为表的ID将根据连接的结果重复。 joinid由BackEnd的ActiveRecord服务自动分配。

5.5交易处理

@ ngrx / data最初有一个类似于事务的过程,称为多重实体,但是这次我们将使用ActiveRecord的事务过程。在事务处理中,预先建立了一个称为"事务"的虚拟实体,并且在该实体的POST处理(即ADD处理)中显示了"事务"参数的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
begin
    Employee.transaction do
      m1 = Organization.new
      m1['org_code'] = "500"
      m1['org_name'] = "新規事業部"
      m1.save!
      e1 = Employee.new
      e1['organization_id'] = m1.id
      e1['position_id'] = 4
      e1['branch_id'] = 1
      e1['first_name'] = "光"
      e1['last_name'] = "伊藤"
      e1['mail_address'] = "[email protected]"
      e1.save!
    end
    @retstatus = "OK"
    @retmessage ="success"
  rescue => e
    @retstatus = "NG"
    @retmessage = "Error occured (#{e.class})"
end

在上面显示的事务中,创建了一个新部门,并在employee表的organization_id中设置了部门代码以创建该表。

5.6计算处理

一些ActiveRecords返回一些计算结果,例如最大值,最小值,总计和平均值。这还会启动一个称为"计算"的虚拟实体,并在那里获取计算结果。在示例程序中,平均分数以英语测试的形式计算。示例程序的查询如下所示。

1
const AverageQueryStr: string = `Employee.average(:english_test)` ;

6小结

请注意,该示例不包含安全措施或错误处理,因为它是由紧急工作创建的。暂时,我可以通过连接@ ngrx / data和ActiveRecord来连接到数据库。 @ ngrx / data相对容易引入,因此我想继续积极使用它。如果您认为这是更好还是不好,请告诉我们。