搜索​​​​

清除过滤器
公告
Claire Zheng · 十月 8, 2024

Global Masters 回归! 启动日期:2024 年 10 月 3 日

各位社区成员,大家好! 我们想念 Global Masters,相信你们也是! 🚀 Global Masters 将于 2024 年 10 月 3 日再次启动! 10 月 3 日,我们将向所有开发者社区成员分享访问新平台的链接。敬请关注开发者社区的专属帖子! 我们一直在紧锣密鼓地筹备这一计划的回归,但如果您发现有任何细节之处做得不到位,请多加包涵,这项工作仍在进行中! 我们将重点放在先提供最有价值的功能:挑战(我们现在称之为“邀请”… 但有时仍然是“挑战”,因为我们在不断调整!)、奖励和徽章。 一些功能仍在开发中: 1. API 集成:Global Masters、开发者社区、Open Exchange 和 Ideas Portal 之间的集成仍在进行中。 预计启动时间是 10 月底。 与此同时,我们正在跟踪您的贡献,并将在功能上线后更新您的积分余额。 2. 级别:新平台不支持级别,但我们正紧锣密鼓地筹备恢复级别功能。 等待已结束!🎉 冲吧!
文章
Michael Lei · 十二月 7, 2022

创建基于 FHIR 的表单

Intersystems IRIS for Health 对 FHIR 行业标准提供了出色的支持。主要特点是:1.FHIR 服务器2. FHIR数据库3. REST 和 ObjectScript API 用于 FHIR 资源(患者、问卷、疫苗等)的 CRUD 操作 本文演示了如何使用这些功能,并展示了用于创建和查看表单类型的 FHIR 资源的Angula前端。 第 1 步 - 使用 InterSystems IRIS for Health 部署您的 FHIR 服务器 要创建 FHIR 服务器,您必须将以下说明添加到 iris.script 文件中(来自:https://openexchange.intersystems.com/package/iris-fhir-template) zn "HSLIB" set namespace= "FHIRSERVER" Set appKey = "/fhir/r4" Set strategyClass = "HS.FHIRServer.Storage.Json.InteractionsStrategy" set metadataPackages = $lb ( "hl7.fhir.r4.core@4.0.1" ) set importdir= "/opt/irisapp/src" //Install a Foundation namespace and change to it Do ##class (HS.HC.Util.Installer).InstallFoundation(namespace) zn namespace // Install elements that are required for a FHIR-enabled namespace Do ##class (HS.FHIRServer.Installer).InstallNamespace() // Install an instance of a FHIR Service into the current namespace Do ##class (HS.FHIRServer.Installer).InstallInstance(appKey, strategyClass, metadataPackages) // Configure FHIR Service instance to accept unauthenticated requests set strategy = ##class (HS.FHIRServer.API.InteractionsStrategy).GetStrategyForEndpoint(appKey) set config = strategy.GetServiceConfigData() set config.DebugMode = 4 do strategy.SaveServiceConfigData(config) zw ##class (HS.FHIRServer.Tools.DataLoader).SubmitResourceFiles( "/opt/irisapp/fhirdata/" , "FHIRServer" , appKey) do $System .OBJ.LoadDir( "/opt/irisapp/src" , "ck" ,, 1 ) zn "%SYS" Do ##class (Security.Users).UnExpireUserPasswords( "*" ) zn "FHIRSERVER" zpm "load /opt/irisapp/ -v" : 1 : 1 //zpm "install fhir-portal" halt 使用实用程序类 HS.FHIRServer.Installer,您可以创建 FHIR 服务器。 第 2 步 - 使用 FHIR REST 或 ObjectScript API 读取、更新、删除和查找 FHIR 数据 我喜欢使用 ObjectScript 类 HS.FHIRServer.Service 来执行所有 CRUD 操作。 要从资源类型(表单)中获取所有 FHIR 数据: /// Retreive all the records of questionnaire ClassMethod GetAllQuestionnaire() As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" set request.RequestMethod = "GET" do fhirService.DispatchRequest(request, .pResponse) set json = pResponse.Json set resp = [] set iter = json.entry. %GetIterator () while iter. %GetNext (.key, .value) { do resp. %Push (value.resource) } write resp. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on get all questionnairies" } Quit tSC } 要从 FHIR 数据存储库中获取特定数据项(调查表): /// Retreive a questionnaire by id ClassMethod GetQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "GET" do fhirService.DispatchRequest(request, .pResponse) write pResponse.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on get the questionnaire" } Quit tSC } 要创建新的 FHIR 资源事件(新的调查表): /// Create questionnaire ClassMethod CreateQuestionnaire() As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" set request.RequestMethod = "POST" set data = {}. %FromJSON ( %request.Content ) set data.resourceType = "Questionnaire" set request.Json = data do fhirService.DispatchRequest(request, .response) write response.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on create questionnaire" } Return tSC } 要更新 FHIR 资源(表单): /// Update a questionnaire ClassMethod UpdateQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "PUT" set data = {}. %FromJSON ( %request.Content ) set data.resourceType = "Questionnaire" set request.Json = data do fhirService.DispatchRequest(request, .response) write response.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on update questionnaire" } Return tSC } 要删除 FHIR 资源事件(表单): /// Delete a questionnaire by id ClassMethod DeleteQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "DELETE" do fhirService.DispatchRequest(request, .pResponse) } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on delete the questionnaire" } Quit tSC } 如您所见,您想要创建使用 POST,更新使用 PUT,删除使用 DELETE,查询使用 GET 动词。 第 3 步 - 创建 Angular 客户端以使用您的 FHIR 服务器应用程序 我使用 PrimeNG 创建了一个角度应用程序并安装了包 npm install --save @types/fhir。此包具有映射到 TypeScript 的所有 FHIR 类型。 Angular控制器类: import { Component, OnInit, ViewEncapsulation } from '@angular/core' ; import { ActivatedRoute, Router } from '@angular/router' ; import { Period, Questionnaire } from 'fhir/r4' ; import { ConfirmationService, MessageService, SelectItem } from 'primeng/api' ; import { QuestionnaireService } from './questionnaireservice' ; const QUESTIONNAIREID = 'questionnaireId' ; @Component({ selector: 'app-questionnaire', templateUrl: './questionnaire.component.html', providers: [MessageService, ConfirmationService], styleUrls: ['./questionnaire.component.css'], encapsulation: ViewEncapsulation.None }) export class QuestionnaireComponent implements OnInit { public questionnaire: Questionnaire ; public questionnairies: Questionnaire[] ; public selectedQuestionnaire: Questionnaire ; public questionnaireId: string ; public sub: any ; public publicationStatusList: SelectItem[] ; constructor( private questionnaireService: QuestionnaireService, private router: Router, private route: ActivatedRoute, private confirmationService: ConfirmationService, private messageService: MessageService){ this.publicationStatusList = [ {label: 'Draft', value: 'draft'}, {label: 'Active', value: 'active'}, {label: 'Retired', value: 'retired'}, {label: 'Unknown', value: 'unknown'} ] } ngOnInit() { this.reset() ; this.listQuestionnaires() ; this.sub = this.route.params.subscribe(params => { this.questionnaireId = String(+params[QUESTIONNAIREID]) ; if (!Number.isNaN(this.questionnaireId)) { this.loadQuestionnaire(this.questionnaireId) ; } }) ; } private loadQuestionnaire(questionnaireId) { this.questionnaireService.load(questionnaireId).subscribe(response => { this.questionnaire = response ; this.selectedQuestionnaire = this.questionnaire ; if (!response.effectivePeriod) { this.questionnaire.effectivePeriod = <Period>{} ; } }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load questionnaire.' }) ; }) ; } public loadQuestions() { if (this.questionnaire && this.questionnaire.id) { this.router.navigate(['/question', this.questionnaire.id]) ; } else { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Choose a questionnaire.' }) ; } } private listQuestionnaires() { this.questionnaireService.list().subscribe(response => { this.questionnairies = response ; this.reset() ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load the questionnaries.' }) ; }) ; } public onChangeQuestionnaire() { if (this.selectedQuestionnaire && !this.selectedQuestionnaire.id) { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' }) ; } else { if (this.selectedQuestionnaire && this.selectedQuestionnaire.id) { this.loadQuestionnaire(this.selectedQuestionnaire.id) ; } } } public reset() { this.questionnaire = <Questionnaire>{} ; this.questionnaire.effectivePeriod = <Period>{} ; } public save() { if (this.questionnaire.id && this.questionnaire.id != "" ) { this.questionnaireService.update(this.questionnaire).subscribe( (resp) => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire saved.' }) ; this.listQuestionnaires() this.loadQuestionnaire(this.questionnaire.id) ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on save the questionnaire.' }) ; } ) ; } else { this.questionnaireService.save(this.questionnaire).subscribe( (resp) => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire saved.' }) ; this.listQuestionnaires() this.loadQuestionnaire(resp.id) ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on save the questionnaire.' }) ; } ) ; } } public delete(id: string) { if (!this.questionnaire || !this.questionnaire.id) { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' }) ; } else { this.confirmationService.confirm({ message: ' Do you confirm?', accept: () => { this.questionnaireService.delete(id).subscribe( () => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire deleted.' }) ; this.listQuestionnaires() ; this.reset() ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on delete questionnaire.' }) ; } ) ; } }) ; } } } Angular HTML 文件 <p-toast [style]= "{marginTop: '80px', width: '320px'}" ></p-toast> <p-card> <div class = "p-fluid p-formgrid grid" > <div class = "field col-12 lg:col-12 md:col-12" > <p-dropdown id= "dropquestions1" [options]= "questionnairies" [(ngModel)]= "selectedQuestionnaire" (onChange)= "onChangeQuestionnaire()" placeholder= "Select a Questionnaire" optionLabel= "title" [filter]= "true" [showClear]= "true" ></p-dropdown> </div> </div> <p-tabView> <p-tabPanel leftIcon= "fa fa-question" header= "Basic Data" > <div class = "p-fluid p-formgrid grid" > <div class = "field col-3 lg:col-3 md:col-12" > <label for = "txtname" >Name</label> <input class = "inputfield w-full" id= "txtname" required type= "text" [(ngModel)]= "questionnaire.name" pInputText placeholder= "Name" > </div> <div class = "field col-7 lg:col-7 md:col-12" > <label for = "txttitle" >Title</label> <input class = "inputfield w-full" id= "txttitle" required type= "text" [(ngModel)]= "questionnaire.title" pInputText placeholder= "Title" > </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtdate" >Date</label> <p-inputMask id= "txtdate" mask= "9999-99-99" [(ngModel)]= "questionnaire.date" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtstatus" >Status</label> <p-dropdown [options]= "publicationStatusList" [(ngModel)]= "questionnaire.status" ></p-dropdown> </div> <div class = "field col-3 lg:col-3 md:col-12" > <label for = "txtpublisher" >Publisher</label> <input class = "inputfield w-full" id= "txtpublisher" required type= "text" [(ngModel)]= "questionnaire.publisher" pInputText placeholder= "Publisher" > </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtstartperiod" >Start Period</label> <p-inputMask id= "txtstartperiod" mask= "9999-99-99" [(ngModel)]= "questionnaire.effectivePeriod.start" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtendperiod" >End Period</label> <p-inputMask id= "txtendperiod" mask= "9999-99-99" [(ngModel)]= "questionnaire.effectivePeriod.end" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-12 lg:col-12 md:col-12" > <label for = "txtcontent" >Description</label> <p-editor [(ngModel)]= "questionnaire.description" [style]= "{'height':'100px'}" ></p-editor> </div> </div> <div class = "grid justify-content-end" > <button pButton pRipple type= "button" label= "New Record" (click)= "reset()" class = "p-button-rounded p-button-success mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Save" (click)= "save()" class = "p-button-rounded p-button-info mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Delete" (click)= "delete(questionnaire.id)" class = "p-button-rounded p-button-danger mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Questions" (click)= "loadQuestions()" class = "p-button-rounded p-button-info mr-2 mb-2" ></button> </div> </p-tabPanel> </p-tabView> </p-card> <p-confirmDialog #cd header= "Atenção" icon= "pi pi-exclamation-triangle" > <p-footer> <button type= "button" pButton icon= "pi pi-times" label= "Não" (click)= "cd.reject()" ></button> <button type= "button" pButton icon= "pi pi-check" label= "Sim" (click)= "cd.accept()" ></button> </p-footer> </p-confirmDialog> 角度服务类 import { Injectable } from '@angular/core' ; import { HttpClient, HttpHeaders } from '@angular/common/http' ; import { Observable } from 'rxjs' ; import { environment } from 'src/environments/environment' ; import { take } from 'rxjs/operators' ; import { Questionnaire } from 'fhir/r4' ; @Injectable({ providedIn: 'root' }) export class QuestionnaireService { private url = environment.host2 + 'questionnaire' ; constructor(private http: HttpClient) { } public save(Questionnaire: Questionnaire): Observable<Questionnaire> { return this.http.post<Questionnaire>(this.url, Questionnaire).pipe(take( 1 )) ; } public update(Questionnaire: Questionnaire): Observable<Questionnaire> { return this.http.put<Questionnaire>(`${this.url}/${Questionnaire.id}`, Questionnaire).pipe(take( 1 )) ; } public load(id: string): Observable<Questionnaire> { return this.http.get<Questionnaire>(`${this.url}/${id}`).pipe(take( 1 )) ; } public delete(id: string): Observable<any> { return this.http.delete(`${this.url}/${id}`).pipe(take( 1 )) ; } public list(): Observable<Questionnaire[]> { return this.http.get<Questionnaire[]>(this.url).pipe(take( 1 )) ; } } 第 4 步 - 实际应用 1. 转到 https://openexchange.intersystems.com/package/FHIR-Questionnaires 应用程序。 2. clone/git pull repo 到任何本地目录 $ git clone https://github.com/yurimarx/fhir-questions.git 3、在该目录下打开终端,运行: $ docker-compose up -d 4.打开网络应用程序: http://localhost:52773/fhirquestions/index.html 截图:
文章
Lilian Huang · 九月 7, 2023

Docker 简介 - 第 2 部分(Docker Compose、Docker File、Docker Volume)

您好!社区的各位老师, 在我的上一篇文章中,我们学习了以下主题: 什么是 Docker? Docker 的一些好处 Docker 是如何工作的? Docker 镜像 Docker容器 Docker 镜像存储库 InterSystems 的 Docker 镜像存储库 Docker安装 Docker 基本命令 使用 Docker 运行 IRIS 社区版 Docker 桌面图形用户界面 在本文中,我们将讨论以下主题: 使用 Docker Compose 文件( YAML 文件) Docker 文件的使用(用于构建 Docker 镜像) Docker 卷的使用 那么让我们开始吧。 1.使用Docker Compose文件( YAML文件) Docker Compose 是一款旨在帮助定义和共享多容器应用程序的工具。使用 Compose,我们可以创建一个 YAML 文件来定义服务,并且通过单个命令,我们可以启动或拆除所有内容。 使用 Compose 的一大优势是能够在文件中定义应用程序堆栈并将其保存在项目存储库的根目录中。您还可以让其他人轻松地为您的项目做出贡献。 他们只需要克隆您的存储库并启动撰写应用程序。 在我的上一篇文章中,我们使用下面提到的命令来创建并启动带有 InterSystems 社区镜像的容器: docker run -d -p 52773:52773 intersystemsdc/iris-community 此时,让我们修改此命令并添加容器名称、映射更多端口并设置重新启动选项: docker run -d -p 52773:52773 -p 53773:53773 -p 1972:1972 --name iris --restart=always intersystemsdc/iris-community 让我为您分解一下上述命令: # docker run command to create and start the container docker run # -d -an option used to start container in deattach mode -d # -p -an option is used to map the ports -p 52773:52773 -p 53773:53773 -p 1972:1972 # name of the container --name iris # set the restart option to always --restart=always # base image intersystemsdc/iris-community 创建撰写文件 在根文件夹中,创建一个名为-----100-----的文件,并写入上述命令,如下所示: #specify docker-compose version version: '3.6' #services/container details services: #Name of the container iris: #Base Image image: intersystemsdc/iris-community #set restart option restart: always #port mapping ports: - 55036 :1972 - 55037 :52773 - 53773 :53773 docker run 命令和 docker-compose 文件的映射如下所示:Docker-compose 文件快照如下所示: 为了运行 docker-compose 文件代码,我们将使用 docker-compose up 命令: docker-compose up -d -----101----- 或 -----102-----:在后台运行命令并将控制返回到终端的选项。 容器已启动。让我们运行“docker ps”命令来列出正在运行的容器 正如您所看到的,我们使用 docker-compose 文件得到了相同的结果。 创建并启动多个容器 在 docker-compose 的帮助下,我们不仅可以运行多个容器,还可以组织并向其添加更多命令。 例如,在下面的 docker-compose 文件中,我们运行 MongoDB 容器和 iris 容器: #specify docker-compose version version: '3.6' #services/container details services: #Name of the container iris: #Base Image image: intersystemsdc/iris-community #set restart option restart: always #port mapping ports: - 55036:1972 - 55037:52773 - 53773:53773 #start MongoDB container mongodb: image: mongo ports: - 27017:27017 让我们运行 docker-compose up 命令 MongoDB 和 iris 容器现已创建并启动。 2.Docker 文件 Docker可以通过读取-----103-----的指令来自动构建镜像。 -----103----- 是一个文本文档,其中包含用户可以在命令行上调用来组装图像的所有命令。 所以,我们的第一个问题很简单。什么是 Dockerfile? Docker 使用它来构建镜像本身。 Dockerfile 本质上是如何创建镜像的构建指令。 与简单存储二进制映像相比,Dockerfile 的优点是自动构建将确保您拥有可用的最新版本。从安全角度来看这是一件好事,因为您希望确保没有安装任何易受攻击的软件。 常用 Docker 文件命令 您可以在下面找到一些最常用的 docker 命令。请注意,所有 docker 命令都必须大写字母。 从 第一个是 FROM 命令,它告诉您映像基于什么。正是这种多层方法使得 Docker 如此高效和强大。在本例中,使用了 iris-community Docker 映像,它再次引用 Dockerfile 来自动化构建过程。 FROM intersystemsdc/iris-community 工作目录 该命令用于设置一个工作目录,我们将在其中复制文件。例如,下面提到的命令将设置 /opt/irisbuild 作为工作目录: WORKDIR /opt/irisbuild 复制 COPY 命令就像听起来一样简单。它可以将文件复制到容器中。通常,我们复制自定义配置文件、应用程序源文件、数据文件等。 #coping Installer.cls to a root of workdir. Don't miss the dot, here. COPY Installer.cls . #copy source files from src folder to src folder in workdir in the docker container. COPY src src #copy source files from data/fhir folder to fhirdata folder in the docker container. COPY data/fhir fhirdata 环境电压 这会设置环境变量,这些变量可以在 Dockerfile 及其调用的任何脚本中使用。-----105-----指令将环境变量-----106-----定义为值-----107----- ENV USER_ID "SYSTEM" ENV USER_PASSWORD "MANAGER" 跑步 -----108-----命令用于在镜像构建过程中执行命令。 #here we give the rights to irisowner user and group which are run IRIS. RUN chown ${ISC_PACKAGE_MGRUSER} : ${ISC_PACKAGE_IRISGROUP} /opt/irisapp #start IRIS and run script in iris.script file RUN iris start IRIS \ && iris session IRIS < /tmp/iris.script 用户 默认情况下,容器以 root 身份运行,这使它们能够完全控制主机系统。随着容器技术的成熟,可能会出现更安全的默认选项。目前,要求 root 对其他人来说是危险的,并且可能不适用于所有环境。您的映像应使用 -----109----- 指令来指定容器运行的非 root用户。如果您的软件没有创建自己的用户,您可以在 Dockerfile 中创建用户和组。 #here we switch user to a root to create a folder and copy files in docker. USER root WORKDIR /opt/irisapp #switching user from root to irisowner, to copy files USER irisowner COPY src src 更多细节请阅读Docker官方文档 3.Docker卷 Docker卷是一个完全由Docker管理的独立文件系统。它作为标准文件或目录存在于数据保存的主机上。 使用 Docker 卷的目的是将数据保留在容器外部,以便可用于备份或共享。 Docker 卷依赖于 Docker 的文件系统,是 Docker 容器和服务保存数据的首选方法。当容器启动时,Docker 会加载只读镜像层,在镜像堆栈顶部添加读写层,并将卷挂载到容器文件系统上。 我们使用 -----110----- 或 -----111----- 标志来允许我们将本地文件挂载到容器中。 卷是保存 Docker 容器生成和使用的数据的首选机制。虽然绑定挂载取决于主机的目录结构和操作系统,但卷完全由 Docker 管理。与绑定挂载相比,卷有几个优点: 卷比绑定安装更容易备份或迁移。 您可以使用 Docker CLI 命令或 Docker API 管理卷。 卷适用于 Linux 和 Windows 容器。 卷可以在多个容器之间更安全地共享。 卷驱动程序允许您将卷存储在远程主机或云提供商上,以加密卷的内容或添加其他功能。 新卷的内容可以由容器预先填充。 Docker Desktop 上的卷比 Mac 和 Windows 主机上的绑定挂载具有更高的性能。 此外,卷通常是比将数据保留在容器的可写层中更好的选择,因为卷在使用时不会增加容器的大小。此外,卷的内容存在于给定容器的生命周期之外。 我们可以在 docker-compose 文件的 services 部分下提及卷。 #specify docker-compose version version: '3.6' #services/container details services: #Name of the container iris: #Base Image image: intersystemsdc/iris-community #set restart option restart: always #port mapping ports: - 55036 :1972 - 55037 :52773 - 53773 :53773 #create a volume volumes: - ./:/irisdev/app 我们已经创建了 irisdev/app 卷。 概括 Docker 是一个功能强大的工具,允许开发人员和 IT 团队在容器化环境中创建、部署和运行应用程序。通过提供可移植性、一致性、可扩展性、资源效率和安全性,Docker 可以轻松地跨不同环境和基础设施部署应用程序。随着容器化的日益普及,Docker正在成为现代软件开发和部署的重要工具。在本文中,我们学习了如何使用 Docker compose(一个 YAML 文件,指定应用程序中每个容器的配置选项)、Docker 文件(用于构建 Docker 镜像)和 Docker 卷(一种用于共享数据的持久数据存储机制) Docker 容器和主机之间的数据。) 感谢您的阅读!
文章
Jeff Liu · 一月 27, 2021

在Caché中使用正则表达式

1.关于本文 就像Caché模式匹配一样,正则表达式也可以在Caché中用来识别文本数据中的模式--只是表达能力更强。本文简要介绍了正则表达式,以及在Caché中如何使用它。这里提供的信息基于各种来源,最值得拜读的是Jeffrey Friedl的《掌握正则表达式》一书,当然还有Caché在线文档。本文无意讨论正则表达式的所有可能性和细节。如果你想了解更多,请参考第5章中列出的信息来源。 使用模式进行文本处理有时会变得很复杂。在处理正则表达式时,我们通常有几种实体:我们正在搜索模式的文本、模式本身(正则表达式)和匹配(文本中与模式匹配的部分)。为了便于区分这些实体,本文档中使用了以下约定。 文本样本以单色字体单独列出,不加引号。 This is a "text string" in which we want to find "something". 除非不明确,否则正文中的正则表达式会以灰色背景显示,如本例。\".*?\". 需要时用不同的颜色突出显示匹配。 这是一个"text string",我们要在其中找到"something"。 代码样本会显示在如下的文本框里: set t=" This is a ""text string"" in which we want to find ""something " set r="\"".*?\""" w $locate(t,r,,tMatch) 2.一些历史(和一些小事)。 在20世纪40年代初,神经生理学家开发了人类神经系统的模型。几年后,一位数学家用一种代数来描述这些模型,他称之为"正则集"。这种代数的符号被命名为"正则表达式"。 1965年,正则表达式第一次在计算机的范畴内被提及。随着qed,一个作为UNIX操作系统一部分的编辑器,正则表达式开始传播。该编辑器后来的版本提供了一个命令序列g/正则表达式/p(全局、正则表达式、打印),在所有文本行中搜索匹配的正则表达式并输出结果。这个命令序列最终成为独立的UNIX命令行程序"grep"。 今天,许多编程语言都存在正则表达式(RegEx)的各种实现(见3.3节)。 3.Regex 101 就像Caché模式匹配一样,正则表达式也可以用来识别文本数据中的模式--只是表达能力更强。下面的章节概述了正则表达式的组成部分,它们的评估和一些可用的引擎,然后在第4章中详细介绍如何使用。 3.1.正则表达式的组成部分 3.1.1.Regex元字符 以下字符在正则表达式中具有特殊意义。 . ( ) [ ] \ ^ $ | 如果你需要将它们作为字面数使用,你需要使用反斜杠来转义。你也可以使用 \Q <literal sequence> \E显式指定字面序列。 3.1.2.文字 普通文本和转义字符被视为字面,例如:。 abc abc \f 换页 \n 换行 \r 回车 \v 标签 \0+三位数(如:0101) 八进制数Caché (ICU)中使用的regex引擎支持八进制数,最高可达\0377(十进制系统为255)。当你从另一个引擎迁移正则表达式时,请确保你了解它如何处理八进制数。 \x+两位数(如:x41) 十六进制数Caché库确实提供了更多处理十六进制数的选项,请参考文档(链接可以在5.8节找到)。 3.1.3.锚 使用锚点,你可以匹配文本/字符串中的位置,例如:。 \A 字符串的开始 \Z 字符串的末端 ^ 文本或行的开始 $ 文末或行末 \b 字词边界 \B 不字界 < 词的开头 #> 词尾 和一些RegEx引擎的行为有所不同,例如,对构成单词的确切定义以及哪些字符被视为单词定界符。 3.1.4.量词 使用正则表达式量词,你可以指定前面的元素可能出现的频率来进行匹配。 {x}正好出现x次 {x,y}最小x,最大y的出现次数。 * 0或更多;相当于{0,}。 +1或更多;相当于{1,}。 ? 0或1 量词很"贪婪",它们会尽可能多地抓取字符。假设我们有下面的文本字符串,想找到带引号的文本。 This is "a text" with "four quotes". 由于选择器的贪婪性质,正则表达式"/".*/"会找到太多的文本。 This is "a text" with "four quotes". 在这个例子中,正则表达式.*试图捕捉尽可能多的位于一对引号之间的字符。然而,由于点选择器 ( .) 也匹配引号,我们没有得到我们想要的结果。 通过一些regex引擎(包括Caché使用的引擎),你可以通过添加一个问号来控制量化符的贪婪程度。因此,正则表达式"\".*?\"现在可以匹配引号中的两部分文本--这正是我们要找的。 This is "a text" with "four quotes". 3.1.5.字符类(范围) 方括号用于指定字符的范围或字符集,例如 [a-zA-Z0-9] 或 [abcd] - 在 regex 中,这被称为字符类。一个范围可以匹配单个字符,所以范围定义中的字符顺序无关紧要--[dbac]返回的匹配结果与[abcd]相同。 要排除一个字符范围,只需在字符范围定义前面加上^(在方括号内!)。[^abc] 匹配除了a, b或c以外的任何字符. 一些regex引擎确实提供了预先定义的字符类(POSIX),例如。 [:alnum:] [a-zA-z0-9] [:alpha:] [a-zA-Z] [:blank:] [\t] … 3.1.6.Groups (组) 正则表达式的部分内容可以使用对括号进行分组。这对于将量化符应用于一组选择符,以及从同一regex内(反向引用)和从调用正则表达式的Caché对象脚本代码(捕获缓冲区)中引用分组都很有用。组可以被嵌套。 下面的regex匹配由一个三位数组成的字符串,后面是一个破折号,然后是3对大写字母和一个数字,后面是一个破折号,然后是与第一部分相同的三位数。 ([0-9]{3})-([A-Z][[0-9]){3}-\1 这个例子展示了如何使用反向引用(见下文)不仅匹配结构,而且匹配内容:反向引用(紫色)告诉引擎在结尾处寻找与开头处相同的三位数数字(黄色)。它还演示了如何将量词应用于更复杂的结构(绿色)。 上面的regex将匹配以下字符串。 123-D1E2F3-123 在这些上面是不匹配的。 123-D1E2F3-456(最后三位数与前三位数不同) 123-1DE2F3-123(中间部分不包含三个字母/数字对) 123-D1E2-123(中间部分只包含两个字母/数字对) 组也会填充所谓的捕获缓冲区(见4.5.1节)。这是一个非常强大的功能,它允许同时匹配和提取信息。 3.1.7. Alternations(交替) 使用管道字符来指定alternations,例如skyfall|done。这允许匹配更复杂的表达式,如3.1.5节中描述的字符类。 3.1.8.回溯引用 后面的引用允许您引用以前定义的组(括号中的选择器)。下面的例子显示了一个正则表达式,它匹配三个必须相等的连续字符。 ([a-zA-Z])/1/1 后面的引用由\x指定,而x代表第x个括号中的表达式。 3.1.9.优先规则 []在()之前 +和? 在序列前:ab等于a(b*),而不是(ab)*。 序列在alternation前:ab|c等于(ab)|c,而不是a(b|c) 3.2.一些理论 正则表达式的评估通常采用以下两种方法之一来实现(这里描述是简化的,请参考第5章中提到的文献进行深入讨论)。 文本驱动(DFA - Deterministic Finite Automaton) 引擎逐字逐句地检查输入文本,并尝试匹配它目前所拥有的内容。当它真正到达输入文本的结尾时,它宣布成功。 Regex-driven (NFA - Non-deterministic Finite Automaton) 引擎会逐一检查正则表达式,并尝试将其应用到文本中。当它真正到达(并匹配)最后一个标记时,它宣布成功。 方法1是确定性的,执行时间只取决于输入文本的长度。正则表达式中选择符的顺序不影响执行时间。 方法2是非决定性的,引擎会遍历正则表达式中选择符的所有组合,直到找到匹配或遇到错误。因此,当它没有找到匹配项时,这种方法特别慢(因为它必须遍历所有可能的组合)。选择符的顺序确实对执行时间有影响。但是,这种方法允许回溯和捕获缓冲区。 3.3.Regex引擎 目前有很多不同的regex引擎,有些是编程语言或操作系统的内置部分,有些是几乎可以在任何地方使用的库。以下是一些regex引擎,按评估方法分组。 DFA: grep, awk, lex. NFA:Perl、Tcl、Python、Emacs、sed、vi、ICU。 下表是各种编程语言和库中可用的regex功能的比较。 详情请点击这里:https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines 4.RegEx和Caché InterSystems Caché使用ICU库来处理正则表达式,Caché在线文档描述了它的许多功能。请参考ICU库的在线文档以了解完整的细节(包括诸如回溯引用等)--ICU的链接可以在5.8节中找到。以下章节旨在为您快速介绍如何使用它。 4.4.$match()和$locate() 在Caché ObjectScript (COS)中,两个函数$match()和$locate()提供了对ICU库提供的大部分regex功能的直接访问。$match(String, Regex) 在输入的字符串中搜索指定的Regex模式。当它找到一个匹配的模式时,它返回1,否则它返回0。 例如: w $match("baaacd",".*(a)\1/1.*") 返回1。 w $match("baacd",".*(a)\1/1.*") 返回0。 $locate(String,Regex,Start,End,Value) 就像$match()一样,以指定的regex模式搜索输入字符串。然而,$locate()给你更多的控制权,它返回更多的信息。在Start中,你可以告诉$locate应该在哪个位置开始搜索输入字符串中的模式。当$locate()找到一个匹配时,它会返回匹配的第一个字符的位置,并将End设置为匹配后的下一个字符位置。匹配的内容会在Value中返回。 如果$locate()没有找到匹配的对象,它将返回0,并且不触及End和Value(如果指定)的内容。End和Value是以引用的形式传递的,所以如果你重复使用它(例如在循环中)要小心。 例如: w $locate("abcdexyz",".d.",1,e,x) 返回3,e设为6,x设为"cde" $locate()执行模式匹配,并且可以同时返回第一个匹配的内容,如果需要提取所有匹配的内容,可以在循环中反复调用$locate(),也可以使用%Regex.Matcher提供的方法。 如果需要提取所有匹配的内容,你可以在一个循环中重复调用$locate(),或者你可以使用%Regex.Matcher提供的方法(见下一节)。 4.5.%Regex.Matcher. %Regex.Matcher提供了ICU库的regex功能,就像$match()和$locate()一样。然而,%Regex.Matcher还提供了一些高级功能,使更复杂的任务变得非常容易使用。下面的章节将重新审视捕获缓冲区,看看用正则表达式替换字符串的可能性以及控制运行时行为的方法。 4.5.1.捕获缓冲区(Buffers) 正如我们在关于组、回溯引用和$locate()的章节中已经看到的,正则表达式允许你同时搜索文本中的模式并返回匹配的内容。它的工作原理是将您想要提取的模式的部分放在一对括号中(组)。匹配成功后,捕获缓冲区包含所有匹配的组的内容。请注意,这与$locate()通过其值参数提供的内容略有不同:$locate()返回整个匹配本身的内容,而捕获缓冲区则让您访问匹配的部分内容(组)。 要使用它,你需要创建一个 %Regex.Matcher 类的对象,并将正则表达式和输入字符串传递给它。然后你可以调用%Regex.Matcher提供的方法来执行实际工作。 例1(简单组): set m=##class(%Regex.Matcher).%New("(a|b).*(de)", "abcdeabcde") w m.Locate()返回1 w m.Group(1) 返回 a w m.Group(2) 返回 de 例2(嵌套组和回溯引用)。 set m=##class(%Regex.Matcher).%New("((a|b).*?(de))(\1)", "abcdeabcde") w m.Match()返回1 w m.GroupCount返回4 w m.Group(1) 返回 abcde。 w m.Group(2) 返回 a w m.Group(3) 返回 de w m.Group(4) 返回 abcde。 (注意嵌套组的顺序--因为开头的括号标志着一个组的开始,所以内部组的索引号比外部组的索引号高) 如前所述,捕获缓冲区是一个非常强大的功能,因为它们允许您同时匹配模式和提取匹配的内容。如果没有正则表达式,您必须在第一步中找到匹配的内容(例如使用模式匹配操作符),并在第二步中根据一些标准提取匹配的内容(或部分内容)。 如果您需要对模式中的部分进行分组(例如对该部分应用量化符),但又不想用匹配部分的内容来填充捕获缓冲区,您可以通过在组前加上问号和冒号的方式将组定义为"非捕获"或"害羞",如下面的例子3。 例3。 set m=##class(%Regex.Matcher).%New("((a|b).*?(?:de))(\1)","abcdeabcde") w m.Match()返回1 w m.Group(1) 返回 abcde。 w m.Group(2) 返回 a w m.Group(3) 返回 abcde。 w m.Group(4) 返回 <REGULAR EXPRESSION>zGroupGet+3^%Regex.Matcher.1。 4.5.2.替换 %Regex.Matcher也提供了立即替换匹配内容的方法。ReplaceAll()和ReplaceFirst()。 set m=##class(%Regex.Matcher).%New(".c.","abcdeabcde") w m.ReplaceAll("xxxx") 返回 axxxxeaxxxxe。 w m.ReplaceFirst("xxxx") 返回 axxxxeabcde。 你也可以在替换字符串中引用组。如果我们在上一个例子的模式中添加一个组,我们可以通过在替换字符串中包含$1来引用它的内容。 set m=##class(%Regex.Matcher).%New(".(c).","abcdeabcde").","abcdeabcde") w m.ReplaceFirst("xx$1xx") 返回 axxcxxeabcde。 使用$0在替换字符串中包含匹配的全部内容。 w m.ReplaceFirst("xx$0xx") 返回 axxbcdxxeabcde。 4.5.3.操作限制(OperationLimit) 在3.2节中,我们了解了评估正则表达式的两种方法(DFA和NFA)。Caché中使用的正则表达式引擎是一个非确定性的有限自动机(NFA)。因此,对给定输入字符串评估各种正则表达式的持续时间可能会有所不同。[1] 您可以使用%Regex.Matcher对象的OperationLimit属性来限制执行单元的数量(所谓的簇)。执行一个簇的确切持续时间取决于你的环境。通常情况下,一个簇的执行持续时间是非常少的几毫秒。默认情况下,OperationLimit被设置为0(无限制)。 4.6.真实世界的例子:从Perl到Caché的迁移。 本节介绍了从Perl迁移到Caché的过程中与正则表达式有关的部分。Perl 脚本实际上由几十个或多或少复杂的正则表达式组成,这些正则表达式被用来匹配和提取内容。 如果Caché中没有regex功能,迁移项目就会变成一项重大工作。然而,Caché中的regex功能是可用的,Perl脚本中的正则表达式几乎可以在Caché中使用,而不需要任何改变。 下面是Perl脚本的一部分。 将正则表达式从 Perl 移到 Caché 的唯一改动是 /i 修饰符(使 regex 不区分大小写)--这必须从 regex 的结尾移到开头。 在Perl中,捕获缓冲区的内容被复制到特殊的变量中(在上面的Perl代码中是$1和$2)。在Perl项目中,几乎所有的正则表达式都使用了这种机制。为了类似于这种机制,我们在Caché对象脚本中写了一个简单的包装方法。它使用 %Regex.Matcher 对文本字符串评估正则表达式,并将捕获缓冲区的内容以列表的形式返回($lb())。 由此产生的Caché对象脚本代码如下。 如果...RegexMatch( tVCSFullName。 "(?i)[\\\/]([^\\^\/]+)[\\\/]ProjectDB[\\\/](.+)[\\\/]archives[\\\/]", .tCaptureBufferList) { set tDomainPrefix=$zcvt($lg(tCaptureBufferList,1), "U") set tDomain=$zcvt($lg(tCaptureBufferList,2), "U") } … Classmethod RegexMatch(pString as %String, pRegex as %String, Output pCaptureBuffer="") { #Dim tRetVal as %Boolean=0。 set m=##class(%Regex.Matcher).%New(pRegex,pString) set m.Locate() { set tRetVal=1 for i=1:1:m.GroupCount { set pCaptureBuffer=pCaptureBuffer_$lb(m.Group(i)) } } quit tRetVal } 5.参考资料 5.7.一般资料 概括信息和教程。 http://www.regular-expressions.info/engine.html 教程和实例。 http://www.sitepoint.com/demystifying-regex-with-practical-examples/ 几种regex引擎的比较。 https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines 常用表达式cheat sheet。 https://www.cheatography.com/davechild/cheat-sheets/regular-expressions/pdf/ 书。 Jeffrey E. F. Friedl:"掌握正则表达式"(见http://regex.info/book.html) 5.8.Caché在线文件 关于Caché中正则表达式的用法概述。 http://docs.intersystems.com/latest/csp/docbook/ DocBook.UI.Page.cls?KEY=GCOS_regexp。 $match()的文档。 http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fmatch $locate()的文档。 http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_flocate %Regex.Matcher的类引用。 http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=%25SYS&CLASSNAME=%25Regex.Matcher 5.9.ICU 如上所述,InterSystems Caché使用ICU引擎。全面的文档可在网上查阅。 http://userguide.icu-project.org/strings/regexp http://userguide.icu-project.org/strings/regexp#TOC-Regular-Expression-Metacharacters http://userguide.icu-project.org/strings/regexp#TOC-Regular-Expression-Operators http://userguide.icu-project.org/strings/regexp#TOC-Replacement-Text http://userguide.icu-project.org/strings/regexp#TOC-Flag-Options 5.10.工具 有许多工具支持开发人员创建正则表达式--其中一些是免费的,另一些则有商业许可。我个人的选择是RegexBuddy(http://www.regexbuddy.com/)--它提供了一套全面的交互式和可视化功能,可以创建和测试不同风味的正则表达式。 thanks for sharing!
文章
姚 鑫 · 三月 27, 2021

第十三章 使用动态SQL(五)

# 第十三章 使用动态SQL(五) # 从结果集中返回特定的值 要从查询结果集中返回特定的值,必须一次一行遍历结果集。 要遍历结果集,请使用`%Next()`实例方法。 (对于单一值,结果对象中没有行,因此`%Next()`返回0,而不是错误。) 然后,可以使用`%Print()`方法显示整个当前行的结果,或者检索当前行的指定列的值。 `%Next()`方法获取查询结果中下一行的数据,并将该数据放入结果集对象的data属性中。 `%Next()`返回1,表示它位于查询结果中的某一行上。 `%Next()`返回0,表示它位于最后一行(结果集的末尾)之后。 每次调用`%Next()`返回1个增量`%ROWCOUNT`; 如果游标定位在最后一行之后(`%Next()`返回0),`%ROWCOUNT`表示结果集中的行数。 如果`SELECT`查询只返回聚合函数,每个`%Next()`设置`%ROWCOUNT=1`。 第一个`%Next()`返回1并设置`%SQLCODE=0`和`%ROWCOUNT=1`,即使表中没有数据; 任何随后的`%Next()`返回0,并设置`%SQLCODE=100`和`%ROWCOUNT=1`。 从结果集中获取一行后,可以使用以下任何一种方式显示该行的数据: - `rset.%Print()`返回查询结果集中当前行的所有数据值。 - `rset.%GetRow()`和`rset.getrows()`以编码列表结构的元素形式从查询结果集中返回一行的数据值。 - `rset.name`按查询结果集中的属性名称、字段名称、别名属性名称或别名字段名称返回数据值。 - `rset.%Get("fieldname")`通过字段名或别名从查询结果集中或存储的查询返回一个数据值。 - `rset.%GetData(n)`按列号从查询结果集中或存储的查询中返回一个数据值。 ## %Print()方法 `%Print()`实例方法从结果集中检索当前记录。默认情况下,`%Print()`在数据字段值之间插入空白空格分隔符。 `%Print()`不会在记录的第一个字段值之前或最后一个字段值之后插入空白; 它在记录的末尾发出一个行返回。 如果数据字段值已经包含空格,则将该字段值括在引号中,以将其与分隔符区分开来。 例如,如果`%Print()`返回城市名称,它将按如下方式返回它们: `"New York" Boston Atlanta "Los Angeles" "Salt Lake City" Washington`. 引用包含分隔符作为数据值一部分的字段值,即使从未使用过`%Print()`分隔符; 例如,如果结果集中只有一个字段。 可以选择指定`%Print()`参数,该参数提供在字段值之间放置的另一个定界符。指定其他定界符将覆盖包含空格的数据字符串的引用。此`%Print()`分隔符可以是一个或多个字符。它指定为带引号的字符串。通常,`%Print()`分隔符最好是在结果集数据中找不到的字符或字符串。但是,如果结果集中的字段值包含`%Print()`分隔符(或字符串),则该字段值将用引号引起来,以将其与分隔符区分开。 如果结果集中的字段值包含换行符,则该字段值将以引号引起来。 以下ObjectScript示例使用`%Print()`遍历查询结果集以显示每个结果集记录,并使用 `"^|^"` 定界符分隔值。请注意`%Print()`如何显示`FavoriteColors`字段中的数据,该字段是元素的编码列表: ```java /// d ##class(PHA.TEST.SQL).ROWCOUNTPrint() ClassMethod ROWCOUNTPrint() { SET q1="SELECT TOP 5 Name,DOB,Home_State,FavoriteColors " SET q2="FROM Sample.Person WHERE FavoriteColors IS NOT NULL" SET myquery = q1_q2 SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() WHILE rset.%Next() { WRITE "Row count ",rset.%ROWCOUNT,! DO rset.%Print("^|^") } WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT } ``` ```java DHC-APP> d ##class(PHA.TEST.SQL).ROWCOUNTPrint() Row count 1 yaoxin^|^54536^|^WI^|^$lb("Red","Orange","Yellow") Row count 2 姚鑫^|^^|^^|^$lb("Red","Orange","Yellow","Green") Row count 3 姚鑫^|^^|^^|^$lb("Red","Orange","Yellow","Green","Green") Row count 4 Isaacs,Roberta Z.^|^^|^^|^$lb("Red","Orange","Yellow","Green","Yellow") Row count 5 Chadwick,Zelda S.^|^50066^|^WI^|^$lb("White") End of data Total row count=5 ``` 下面的示例显示如何将包含定界符的字段值括在引号中。在此示例中,大写字母`A`用作字段定界符;因此,任何包含大写字母A的字段值(名称,街道地址或州缩写)都将以引号引起来。 ```java /// d ##class(PHA.TEST.SQL).ROWCOUNTPrint2() ClassMethod ROWCOUNTPrint2() { SET myquery = "SELECT TOP 25 Name,Home_Street,Home_State,Age FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() WHILE rset.%Next() { DO rset.%Print("A") } WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).ROWCOUNTPrint2() yaoxinA889 Clinton DriveAWIA30 xiaoliAAA 姚鑫AAA7 姚鑫AAA7 姚鑫AAA43 姚鑫AAA 姚鑫AAA Isaacs,Roberta Z.AAA Chadwick,Zelda S.A9889 Clinton DriveAWIA43 Fives,James D.A2091 Washington BlvdANDA88 Vonnegut,Jose P.A3660 Main PlaceAWIA47 Chadbourne,Barb B.A1174 Second StreetA"VA"A93 "Quigley,Barb A."A"6501 Ash Avenue"AKYA73 ``` ## %GetRow()和%GetRows()方法 `%GetRow()`实例方法从结果集中检索当前行(记录),作为字段值元素的编码列表: ```java /// d ##class(PHA.TEST.SQL).ROWCOUNTPrint3() ClassMethod ROWCOUNTPrint3() { SET myquery = "SELECT TOP 17 %ID,Name,Age FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() FOR { SET x=rset.%GetRow(.row,.status) IF x=1 { WRITE $LISTTOSTRING(row," | "),! } ELSE { WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT RETURN } } } ``` `%GetRows()`实例方法从结果集中检索指定大小的一组行(记录)。每行作为字段值元素的编码列表返回。 下面的示例返回结果集中的第1、6和11行。在此示例中,`%GetRows()`第一个参数(5)指定`%GetRows()`应该检索五行的连续组。如果成功检索到一组五行,`%GetRows()`将返回1。 `.rows`参数通过引用传递这五行的下标数组,因此,`rows(1)`返回每五组中的第一行:第1、6和11行。指定`rows(2)`将返回第2、7行和12。 ```java /// d ##class(PHA.TEST.SQL).ROWCOUNTPrint4() ClassMethod ROWCOUNTPrint4() { SET myquery = "SELECT TOP 17 %ID,Name,Age FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() FOR { SET x=rset.%GetRows(5,.rows,.status) IF x=1 { WRITE $LISTTOSTRING(rows(1)," | "),! } ELSE { WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT RETURN } } } ``` 可以使用`ZWRITE rows`命令返回检索到的数组中的所有下标,而不是按下标检索单个行。请注意,上面的示例ZWRITE行不会返回结果集中的第16行和第17行,因为在检索到最后一组五行之后,这些行是余数。 ## rset.name属性 当InterSystems IRIS生成结果集时,它将创建一个结果集类,其中包含一个与该结果集中的每个字段名称和字段名称别名相对应的唯一属性。 可以使用`rset.name`属性按属性名称,字段名称,属性名称别名或字段名称别名返回数据值。 - 属性名称:如果未定义字段别名,则将字段属性名称指定为`rset.PropName`。结果集字段属性名称取自表定义类中的相应属性名称。 - 字段名称:如果没有定义字段别名,请将字段名称(或属性名称)指定为`rset。“fieldname”`。这是表定义中指定的`SQLFIELDNAME`。 Intersystems Iris使用此字段名称来查找相应的属性名称。在许多情况下,属性名称和字段名称(`SQLFieldName`)是相同的。 - 别名属性名称:如果定义了字段别名,则将别名属性名称指定为`rset.AliasProp`。别名属性名称是根据`SELECT`语句中的列名称别名生成的。不能为具有已定义别名的字段指定字段属性名称。 - 别名:如果定义了字段别名,则将此别名(或别名属性名称)指定为`rset。“ alias”`。这是`SELECT`语句中的列名别名。您不能为具有已定义别名的字段指定字段名称。 - 集合,表达式或子查询:InterSystems IRIS为这些选择项分配一个字段名称`Aggregate_n`,`Expression_n`或`Subquery_n`(其中整数`n`对应于查询中指定的选择项列表的顺序)。可以使用字段名称(`rset。“ SubQuery_7”`不区分大小写),相应的属性名称(`rset.Subquery7`区分大小写)或用户定义的字段名称别名来检索这些select-item值。也可以只使用`rset。%GetData(n)`指定选择项的序列号。 指定属性名称时,必须使用正确的字母大小写;指定字段名称时,不需要正确的字母大小写。 使用属性名称对`rset.name`的调用具有以下后果: - 字母大小写:属性名称区分大小写。字段名称不区分大小写。 Dynamic SQL可以自动解决指定字段或别名与相应属性名称之间的字母大小写差异。但是,解决字母大小写需要时间。为了最大限度地提高性能,应该指定属性名称或别名的确切字母大小写。 - 非字母数字字符:属性名称只能包含字母数字字符(起始的`%`字符除外)。如果相应的SQL字段名称或字段名称别名包含非字母数字字符(例如`Last_Name`),则可以执行以下任一操作: - 指定用引号分隔的字段名称。例如,`rset。“ Last_Name”`)。分隔符的这种使用不需要启用分隔符。执行大写字母解析。 - 指定相应的属性名称,以消除非字母数字字符。例如,`rset.LastName`(或`rset。“ LastName”`)。必须为属性名称指定正确的字母大小写。 - `%`属性名称:通常,以`%`字符开头的属性名称保留供系统使用。如果字段属性名称或别名以`%`字符开头,并且该名称与系统定义的属性冲突,则返回系统定义的属性。例如,对于`SELECT Notes AS%Message`,调用`rset。%Message`将不返回`Notes`字段值。它返回为语句结果类定义的`%Message`属性。可以使用`rset。%Get(“%Message”)`返回字段值。 - 列别名:如果指定了别名,则Dynamic SQL始终匹配该别名,而不匹配字段名称或字段属性名称。例如,对于`SELECT Name AS Last_Name`,只能使用`rset.LastName`或`rset。“ Last_Name”`来检索数据,而不能使用`rset.Name`。 - 重复名称:如果名称解析为相同的属性名称,则它们是重复的。重复名称可以是对表中同一字段的多个引用,对表中不同字段的别名引用或对不同表中字段的引用。例如,`SELECT p.DOB,e.DOB`指定两个重复的名称,即使这些名称引用了不同表中的字段。 如果`SELECT`语句包含相同字段名称或字段名称别名的多个实例,则`rset.propname`或`rset。“fieldname”`始终返回`SELECT`语句中指定的第一个。例如,对于`SELECT C.NAME,P.NAME`来自`Sample.person as p,sample.company`使用`rset.name`检索公司名称字段数据;选择`C.Name,P.Name`作为来自`Sample.person`的名称,`As P,Sample.com`本文使用`RSET。“name”`还检索公司名称字段数据。如果查询中存在重复的名称字段,则字段名称(名称)的最后一个字符由字符(或字符)替换为创建唯一属性名称。因此,查询中的重复名称字段名称具有相应的唯一属性名称,以`NAM0`(第一个重复)通过`NAM9`开始,并通过`NAMZ`继续大写字母`NAMA`。 对于使用`%Prepare()`准备的用户指定的查询,可以单独使用属性名称。对于使用`%PrepareClassQuery()`准备的存储查询,必须使用`%Get(“ fieldname”)`方法。 下面的示例返回由属性名称指定的三个字段的值:两个属性值分别由属性名称和第三个属性值由别名属性名称。在这些情况下,指定的属性名称与字段名称或字段别名相同: ```java /// d ##class(PHA.TEST.SQL).PropSQL() ClassMethod PropSQL() { SET myquery = "SELECT TOP 5 Name,DOB AS bdate,FavoriteColors FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New(1) SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() WHILE rset.%Next() { WRITE "Row count ",rset.%ROWCOUNT,! WRITE rset.Name WRITE " prefers ",rset.FavoriteColors WRITE " birth date ",rset.bdate,!! } WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).PropSQL() Row count 1 yaoxin prefers Red,Orange,Yellow birth date 1990-04-25 Row count 2 xiaoli prefers birth date Row count 3 姚鑫 prefers birth date 2014-01-02 Row count 4 姚鑫 prefers birth date 2014-01-02 Row count 5 姚鑫 prefers birth date 1978-01-28 End of data Total row count=5 ``` 在上面的示例中,返回的字段之一是`FavoriteColors`字段,其中包含`%List`数据。若要显示此数据,`%New(1)`类方法将`%SelectMode`属性参数设置为1(ODBC),从而导致该程序将`%List`数据显示为逗号分隔的字符串,并以ODBC格式显示出生日期: 下面的示例返回`Home_State`字段。因为属性名称不能包含下划线字符,所以本示例指定用引号`(“ Home_State”)`分隔的字段名称`(SqlFieldName)`。还可以指定不带引号的相应生成的属性名称`(HomeState)`。请注意,定界字段名称`(“ Home_State”)`不区分大小写,但是生成的属性名称`(HomeState)`是区分大小写的: ```java /// d ##class(PHA.TEST.SQL).PropSQL1() ClassMethod PropSQL1() { SET myquery = "SELECT TOP 5 Name,Home_State FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New(2) SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT} SET rset = tStatement.%Execute() WHILE rset.%Next() { WRITE "Row count ",rset.%ROWCOUNT,! WRITE rset.Name WRITE " lives in ",rset."Home_State",! } WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).PropSQL1() Row count 1 yaoxin lives in WI Row count 2 xiaoli lives in Row count 3 姚鑫 lives in Row count 4 姚鑫 lives in Row count 5 姚鑫 lives in End of data Total row count=5 ``` %GetRows 这个方法在IRIS 2019里有吗?
文章
Hao Ma · 一月 15, 2021

IAM实践指南——OAuth 2.0下的API保卫战(第二部分)

在这个由三部分组成的系列文章中,我们将展示如何在OAuth 2.0标准下使用IAM简单地为IRIS中的未经验证的服务添加安全性。 在第一部分中,我们介绍了一些OAuth 2.0背景知识,以及IRIS和IAM的初始定义和配置,以帮助读者理解确保服务安全的整个过程。 现在,本文将详细讨论和演示配置IAM所需的步骤——验证传入请求中的访问令牌,并在验证成功时将请求转发到后端。 本系列的最后一部分将讨论和演示IAM生成访问令牌(充当授权服务器)并对其进行验证时所需的配置,以及一些重要的最终考虑事项。 如果您想试用IAM,请联系InterSystems销售代表。 场景1:IAM作为访问令牌验证器 在该场景中,需要使用一个外部授权服务器生成JWT(JSON Web Token)格式的访问令牌。该JWT使用了RS256算法和私钥签名。为了验证JWT签名,另一方(本例中是IAM)需要拥有授权服务器提供的公钥。 由外部授权服务器生成的JWT主体中还包括一个名为“exp”的声明(包含该令牌过期的时间戳),以及另一个名为“iss”的声明(包含授权服务器的地址)。 因此,IAM需要先使用授权服务器的公钥和JWT内部“exp”声明中包含的过期时间戳对JWT签名进行验证,然后再将请求转发给IRIS。 对IAM进行相应配置时,首先要向IAM中的“SampleIRISService”添加一个名为“JWT”的插件。为此,请转到IAM中的Services页面并复制“SampleIRISService”的ID,稍后会用到。 之后,打开插件,点击“New Plugin”按钮,找到“JWT”插件,点击启用。 在下个页面中,将“SampleIRISService”ID粘贴在“service_id”字段中,然后在“config.claims_to_verify”参数中选中“exp”框。 注意,“config.key_claim_name”参数的值是“iss”。后面会用到。 然后,点击“Create”按钮。 完成操作后,找到左侧菜单中的“Consumers”部分,然后单击先前创建的“ClientApp”。点击“Credentials”标签,然后单击按钮“New JWT Credential”。 在下一页中,选择JWT签名算法(本例中为RS256),并将公钥(这是授权服务器提供的PEM格式的公钥)粘贴到“rsa_public_key”字段中。 在“key”字段中,在添加JWT插件时需要用到之前在“config.key_claim_name”字段中输入的JWT声明内容。所以在本例中,需要插入的是JWT的iss声明内容(本例中是授权服务器的地址)。 之后,单击“Create”按钮。 提示:出于调试目的,可以使用一个在线工具对JWT进行解码,将公钥粘贴进去就可以检查声明内容及其值,并且验证签名。该在线工具的链接如下:https://jwt.io/#debugger 现在,添加了JWT插件后,就不能发送未经身份验证的请求了。如下所示,对URL的一个简单GET请求(未经身份验证): http://iamhost:8000/event/1 返回一个未经授权的信息,以及状态码“401未经授权”。 为了从IRIS获得结果,需要将JWT添加到请求中。 首先,需要向授权服务器请求JWT。如果POST请求与主体中的一些键值对(包括用户和客户端信息)一起发出,那么在这里使用的自定义授权服务器将向以下URL返回一个JWT: https://authorizationserver:5001/auth 该请求及其响应如下所示: 然后,可以将响应中获得的JWT添加到授权标头中作为Bearer令牌使用,并将GET请求发送到和之前相同的URL: http://iamhost:8000/event/1 或者将它作为querystring参数添加进去。当添加JWT插件(本例中是“jwt”)时,querystring关键字是在“config.uri_param_names”字段中指定的值 最后,如果在“config.cookie_names”字段中输入任意名称,选择将JWT作为cookie包含在请求中。 请继续阅读本系列的第三部分也即最后一部分,了解IAM生成和验证访问令牌所需的配置,以及一些重要的最终考虑因素。
文章
姚 鑫 · 六月 22, 2021

第十五章 XML检查属性

# 第十五章 XML检查属性 # 检查属性的基本方法 可以使用`%XML.Node`的以下方法。以检查当前节点的属性。 - `AttributeDefined()` 如果当前元素具有具有给定名称的属性,则返回非零(TRUE)。 - `FirstAttributeName()` 返回当前元素的第一个属性的属性名称。 - `GetAttributeValue()` 返回给定属性的值。如果元素没有该属性,则该方法返回NULL。 - `GetNumberAttributes()` 返回当前元素的属性数。 - `LastAttributeName()` 返回当前元素的最后一个属性的属性名称。 - `NextAttributeName()` 在给定属性名称的情况下,无论指定的属性是否有效,此方法都会按排序顺序返回下一个属性的名称。 - `PreviousAttributeName()` 在给定属性名称的情况下,无论指定的属性是否有效,此方法都会按排序顺序返回上一个属性的名称。 下面的示例遍历给定节点中的属性并编写一个简单报表: ```java /// d ##class(Demo.XmlDemo).ShowAttributes("David Marston") /// David Marston ClassMethod ShowAttributes(string) { set reader=##class(%XML.Reader).%New() set status=reader.OpenString(string) if $$$ISERR(status) {do $System.Status.DisplayError(status)} s node = reader.Document.GetDocumentElement() b s count = node.GetNumberAttributes() w !, "属性数量: ", count s first = node.FirstAttributeName() w !, "第一个属性是: ", first w !, " 值是: ",node.GetAttributeValue(first) s next = node.NextAttributeName(first) for i = 1 : 1 : count - 2 { w !, "下一个属性是: ", next w !, " 值是: ",node.GetAttributeValue(next) s next = node.NextAttributeName(next) } s last = node.LastAttributeName() w !, "最后一个属性是: ", last w !, " 值是: ",node.GetAttributeValue(last) } ``` 示例XML文档: ```xml David Marston ``` 如果将此文档的第一个节点传递给示例方法,则会看到以下输出: ```java Number of attributes: 5 First attribute is: attr1 Its value is: first Next attribute is: attr2 Its value is: second Next attribute is: attr3 Its value is: third Next attribute is: attr4 Its value is: fourth Last attribute is: attr5 Its value is: fifth ``` # 检查属性的其他方法 本节讨论可用于获取任何属性的名称、值、命名空间、`QName`和值命名空间的方法。这些方法分为以下几组: - 仅使用属性名称的方法 - 使用属性名称和命名空间的方法 注意:在XML标准中,一个元素可以包含多个同名的属性,每个属性位于不同的名称空间中。但是,在InterSystems IRIS XML中,这是不受支持的。 ## 仅使用属性名称的方法 使用以下方法获取有关属性的信息。 ### GetAttribute() ```java method GetAttribute(attributeName As %String, ByRef namespace As %String, ByRef value As %String, ByRef valueNamespace As %String) ``` 返回给定属性的数据。此方法通过引用返回下列值: - `Namespace`是来自属性QName的命名空间URI - `value` 是属性值。 - `valueNamespace` 值所属的命名空间URI。例如,以下属性: ``` xsi:type="s:string" ``` 此属性的值为字符串,并且此值位于使用前缀s在其他位置声明的命名空间中。假设本文档的较早部分包含以下命名空间声明: ```java xmlns:s="http://www.w3.org/2001/XMLSchema" ``` 在本例中,`valueNamespace`将为`“http://www.w3.org/2001/XMLSchema”`. ### GetAttributeNamespace() ```java method GetAttributeNamespace(attributeName As %String) as %String ``` 从当前元素的名为`AttributeName`的属性的`QName`返回命名空间URI。 ### GetAttributeQName() ```java method GetAttributeQName(attributeName As %String) as %String ``` 返回给定属性的`QName`。 ### GetAttributeValue() ```java method GetAttributeValue(attributeName As %String) as %String ``` 返回给定属性的值。 ### GetAttributeValueNamespace() ```java method GetAttributeValueNamespace(attributeName As %String) as %String ``` 返回给定属性的值的命名空间。 ## 使用属性名和命名空间的方法 要同时使用属性名称及其命名空间来获取有关属性的信息,请使用以下方法: ### GetAttributeNS() ```java method GetAttributeNS(attributeName As %String, namespace As %String, ByRef value As %String, ByRef valueNamespace As %String) ``` 返回给定属性的数据,其中`AttributeName`和`Namespace`指定感兴趣的属性。此方法通过引用返回以下数据: - `value` 是属性值。 - `valueNamespace` 值所属的命名空间URI。例如,以下属性: ```java xsi:type="s:string" ``` 此属性的值为字符串,并且此值位于使用前缀s在其他位置声明的命名空间中。假设本文档的较早部分包含以下命名空间声明: ```java xmlns:s="http://www.w3.org/2001/XMLSchema" ``` ### GetAttributeQNameNS() ```java method GetAttributeQNameNS(attributeName As %String, namespace As %String) as %String ``` 返回给定属性的`QName`,其中`AttributeName`和`Namespace`指定感兴趣的属性。 ### GetAttributeValueNS() ```java method GetAttributeValueNS(attributeName As %String, namespace As %String) as %String ``` 返回给定属性的值,其中`AttributeName`和`Namespace`指定感兴趣的属性。 ### GetAttributeValueNamespaceNS ```java method GetAttributeValueNamespaceNS(attributeName As %String, namespace As %String) as %String ``` 返回给定属性的值的命名空间,其中`AttributeName`和`Namespace`指定感兴趣的属性。
文章
姚 鑫 · 五月 31, 2021

第十二章 IBM WebSphere MQ检索邮件

# 第十二章 IBM WebSphere MQ检索邮件 # 检索邮件 要检索邮件,请执行以下操作: 1. 按照“创建连接对象”中的说明创建连接对象。在这种情况下,请创建`%Net.MQRecv`的实例。`Connection`对象有一个消息队列,可以从中检索消息。 2. 根据需要调用以下方法: - `%Get()`-通过引用返回字符串消息作为第一个参数。 - `%GetStream()`-给定初始化的文件字符流,此方法从队列中检索消息,并将其放入与该流关联的文件中。请注意,必须设置流的`Filename`属性才能对其进行初始化。不支持二进制流。 3. 检查调用的方法返回的值。请参阅“获取错误代码”。请记住,当队列为空时,IBM `WebSphere MQ`返回`2033`。 4. 检索完消息后,调用`Connection`对象的`%Close()`方法以释放动态链接库的句柄。 示例1:`ReceiveString()` 下面的类方法从`mqtest`队列检索消息。 ```java ///Method returns string or null or error message ClassMethod ReceiveString() As %String { Set recv=##class(%Net.MQRecv).%New() Set queue="mqtest" Set qm="QM_antigua" Set chan="S_antigua/TCP/antigua(1414)" Set logfile="c:\mq-recv-log.txt" Set check=recv.%Init(queue,qm,chan,logfile) If 'check Quit recv.%GetLastError() Set check=recv.%Get(.msg) If 'check { Set reasoncode=recv.%GetLastError() If reasoncode=2033 Quit "" Quit "ERROR: "_reasoncode } Quit msg } ``` 示例2:`ReceiveCharacterStream()` 以下方法可以检索更长的消息,因为它使用`%GetStream()`: ```java /// Method returns reason code from IBM WebSphere MQ ClassMethod ReceiveCharacterStream() As %Integer { Set recv=##class(%Net.MQRecv).%New() Set queue="mqtest" Set qm="QM_antigua" Set chan="S_antigua/TCP/antigua(1414)" Set logfile="c:\mq-recv-log.txt" Set check=recv.%Init(queue,qm,chan,logfile) If 'check Quit recv.%GetLastError() //initialize the stream and tell it what file to use //make sure filename is unique we can tell what we received Set longmsg=##class(%FileCharacterStream).%New() Set longmsg.Filename="c:\mq-received"_$h_".txt" Set check=recv.%GetStream(longmsg) If 'check Quit recv.%GetLastError() Quit check } ``` # 更新消息信息 `%Net.MQSend`和`%Net.MQRecv`类还提供以下方法: ### %CorId() (通过引用)更新上次读取的邮件的关联ID。 ### %ReplyQMgrName() (通过引用)更新上次读取的消息的回复队列管理器名称。 ### %ReplyQName() (通过引用)更新上次读取的消息的回复队列名称。 # Troubleshooting 如果在使用IBM `WebSphere MQ`的InterSystems IRIS接口时遇到问题,应该首先确定客户端是否安装正确并且可以与服务器通信。要执行这样的测试,可以使用IBM `WebSphere MQ`提供的示例程序。可执行文件位于IBM `WebSphere MQ`客户端的bin目录中。 以下步骤介绍如何在`Windows`上使用这些示例程序。在其他操作系统上,细节可能会有所不同;请参考IBM文档并检查您的客户端中存在的文件的名称。 1. 创建一个名为`MQSERVER`的环境变量。它的值的格式应该是`channel_name/Transport/server`,其中`channel_name`是要使用的通道的名称,`Transport`是指示要使用的传输的字符串,而`server`是服务器的名称。例如:`S_Antigua/TCP/Antigua` 2. 在命令行中,输入以下命令: ```java amqsputc queue_name queue_manager_name ``` 其中,`QUEUE_NAME`是要使用的队列的名称,`QUEUE_MANAGER_NAME`是队列管理器的名称。例如: ```java amqsputc mqtest QM_antigua ``` 如果`amqsputc`命令无法识别,请确保已更新`PATH`环境变量以包括IBM `WebSphere MQ`客户端的bin目录。 3. 应该会看到几行代码,如下所示: ```java Sample AMQSPUT0 start target queue is mqtest ``` 4. 现在可以发送消息了。只需键入每条消息,然后在每条消息后按Enter键即可。例如: ```java sample message 1 sample message 2 ``` 5. 发送完邮件后,按两次Enter键。然后,将看到如下所示的行: ```java Sample AMQSPUT0 end ``` 6. 要完成此测试,我们将检索发送到队列的消息。在命令行中键入以下命令: ```java amqsgetc queue_name queue_manager_name ``` 其中,`QUEUE_NAME`是要使用的队列的名称,`QUEUE_MANAGER_NAME`是队列管理器的名称。例如: 7. 然后,应该看到一个起始行,后跟之前发送的消息,如下所示: ```java Sample AMQSGET0 start message message ``` 8. 此示例程序短暂等待接收任何其他消息,然后显示以下内容: ```java no more messages Sample AMQSGET0 end ``` 如果测试失败,请参考IBM文档。问题的可能原因包括以下几个方面: - 安全问题 - 队列定义不正确 - 队列管理器未启动
文章
Hao Ma · 十一月 17, 2021

开发Ensemble REST服务

REF: https://docs.intersystems.com/healthconnectlatest/csp/docbook/Doc.View.cls?KEY=GREST REF: https://docs.intersystems.com/healthconnectlatest/csp/docbook/DocBook.UI.Page.cls?KEY=AFL_rest#AFL_C4838 开发REST服务有两个方式, 一个是生生的写代码, 定义接口的标准,被称为"Manually Coding"。第2个方式是目前越来越流行的"Sepcification-first",也就是使用描述性的语言定义接口规范,然后通过这个规范生成接口代码。第2种方式更快捷,但这里我还是从第一种介绍起,对理解里面的代码层次更容易一些,而这是调试一个接口必须的。 从代码开发REST服务 不同于HTTP和SOAP, Ensemble里面没有REST的inbound Adaptor,也没有可用的BS组件。在Production里开发一个REST服务的步骤是: 1. 开发一个REST Service, 这个Service是一个CSP Page, 是一个网页服务,和Ensemble没关系。要在Production中使用这个服务,您需要在这个服务里调用一个Production的业务服务BS。 2. 要访问这个REST页面服务, 您需要配置一个Web Application。Web Application的配置项上有一个选项: "REST 分派类"。这样配置好之后, Web Application收到相应的URL后就会调用这个REST页面,页面再去调用Production的BS。 3. 最后,您需要在BS中处理收到的JSON, 发送给其他组件,以传递给接收方系统。 如果您看的了代码包里的EnsLib.REST.Service类, 它继承了%CSP.REST页面, 也继承了BusinessService,非常符合Ensemble的结构设计。But, 别用。在线文档中有专门的说明。 Although InterSystems IRIS defines a class EnsLib.REST.ServiceOpens in a new window, that is a subclass of %CSP.RESTOpens in a new window, we recommend that you not use this class because it provides an incomplete implementation of %CSP.REST Opens in a new window. 让我们开始开发一个简单的REST服务并加入Production: Step 1: 创建以下代码,解释一下: - 继承%CSP.REST,这是个专用于REST的CSP页面 - UrlMap是一个XData, 在COS语言里用于在代码里放置固定的xml数据结构。UrlMap定义从收到的URL到本类里不同的方法之间的映射。 - 方法中入参可以是任意的数据结构和用户定义的类结构,不需要出参。如果直接返回消息给调用者,直接"write"一个流或者字符串 Class SEDemo.IO.REST.SampleService Extends %CSP.REST { XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ] { } ClassMethod Test(pInput As %String) As %Status { write "Received: "_pInput,! Quit 1 } ClassMethod GetPatientById(pID As %String) As %Status { Try{ Set tObj=##class(SEDemo.Common.Patient).%OpenId(pID),tStream = "" d ##class(%ZEN.Auxiliary.jsonProvider).%WriteJSONStreamFromObject(.tStream,tObj) w tStream.Read() } Catch (e) {Set tSC=e.AsStatus()} Quit tSC } } Step 2: 创建Web Application 在管理界面System Administration > Security > Applications > Web Applications,创建一个用于接收此REST服务的Web APPlication, 设置"Dispatch Class"为当前类。 假设创建的Web Applicaiton为"/CSP/myrest",注意: - 选中“Enable Application" - 权限: 分配本命名空间数据库的资源,默认是%DB_%Default。 后面会详细介绍权限和用户管理的细节。 Step 3: 测试你的REST service 你可以选择自己喜欢的测试方式,比如用浏览器,POSTMAN, SoapUI..., 下面是我测试的记录: CNMBPHMA:~ hma$ curl -v http://172.16.58.200:52773/csp/myrest/Test/333 * Trying 172.16.58.200... * TCP_NODELAY set * Connected to 172.16.58.200 (172.16.58.200) port 52773 (#0) > GET /csp/myrest/Test/333 HTTP/1.1 > Host: 172.16.58.200:52773 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 14 Jul 2021 06:47:26 GMT < Server: Apache < CACHE-CONTROL: no-cache < EXPIRES: Thu, 29 Oct 1998 17:04:19 GMT < PRAGMA: no-cache < CONTENT-LENGTH: 15 < Content-Type: text/html; charset=utf-8 < Received: 333 * Connection #0 to host 172.16.58.200 left intact CNMBPHMA:~ hma$ 这里是一个匿名访问,如果需要用户认证,修改一下重发: CNMBPHMA:~ hma$ curl -u 'superuser:SYS' http://172.16.58.200:52773/csp/myrest/Test/333 Received: 333 CNMBPHMA:~ hma$ 注意两点: 1. 到目前为止我们测试的其实是一个HTTP请求和响应,虽然内部用了%CSP.REST的类, 但响应中'Content-Type'还是'text/html' 2. 代码中没有处理出错和查不到结果的情况 3. 到目前为止和Ensemble Production没有任何关系。 Step 4: 将服务加入Ensemble Production 加入Production的意思实际上时调用一个Production的BusinessService。 让我们先创建一个简单的Service. ///不使用Adapter, 收到任何请求,同步发送给目标组件 Class Test.BS.GeneralService Extends Ens.BusinessService { Method OnProcessInput(pInput As %RegisteredObject, Output pOutput As %RegisteredObject) As %Status { set tRequest=##class(Ens.StringRequest).%New() Set tStatus = ..SendRequestSync("Test.BO.dummyOperation", tRequest, .tResponse) set pOutput = tResponse Quit tStatus } } 当需要前面的REST服务来调用这个BusinessService的时候, 需要在method里面加入直接调用的语句,比如上面的GetPatientById() ClassMethod GetPatientById(pID As %String) As %Status { set status = ##class(Ens.Director).CreateBusinessService("Test.BS.GeneralService", .tService) if $$$ISOK(status) { set status = service.OnProcessInput(pID, .tResponse) } w tResponse,! }
文章
Hao Ma · 五月 15, 2024

IRIS/Caché SQL优化经验分享 - 优化关键字

SQL查询优化器一般情况下能给出最好的查询计划,但不是所有情况都这样,所以InterSystems SQL还提供了一个方式, 也就是在查询语句里加入`optimize-option keyword(优化关键字)`, 用来人工的修改查询计划。 比如下面的查询: ```sql SELECT AVG(SaleAmt) FROM %PARALLEL User.AllSales GROUP BY Region ``` 其中的%PARALLEL, 就是最常用的优化关键字, 它强制SQL优化器使用多进程并行处理这个SQL。 您可以这样理解: 如果查询优化器足够聪明,那么绝大多数情况下,根本就不需要优化关键字来人工干预。因此,您也一定不奇怪在不同的IRIS/Caché版本中, 关键字的表现可能不一样。越新的版本,应该是越少用到。比如上面的%PARALLEL, 在Caché的大多数版本中, 在查询中加上它一般都能提高查询速度,而在IRIS中,尤其是2023版本以后, 同样的SQL查询语句,很大的可能查询优化器已经自动使用多进程并行查询了,不再需要用户人工干预了。 因此,先总结有关优化关键字的要点: 1. 优化关键字主要是FROM语句中使用。 UPDATE, INSERT语句也有可以使用的关键字,比如%NOJOURAL等等, 这里我不介绍了,请各位自己查询文档。 > INSERT, UPDATE的关键字常用的有:%NOCHECK %NOINDEX %NOLOCK %NOTRIGGER 等等 2. 各个不同版本的文档中这部分内容有少许的不同。 3. 使用查询关键字要结合阅读查询计划,需要经验的积累。用的多了, 在当前版本什么样的查询需要添加关键字就比较有数了。 最新版本的联机文档在: [Specify Optimization Hints in Queries | Configure SQL Performance Options](https://docs.intersystems.com/iris20241/csp/docbook/DocBook.UI.Page.cls?KEY=GSOC_hints#GSOC_hints_clausekeys) ## %PARALLEL 指定查询使用多个进程并行处理。在Query Plan中您可以得到证实。有关Query Plan的阅读请看前面的帖子。 ## %IGNOREINDEX 指定不用某一个或者某几个index。比如以下查询: ```sql select min(ps_supplycost) from %PARALLEL %IGNOREINDEX SQLUser.supplier.SUPPLIER_PK %IGNOREINDEX SQLUser.part.PART_PK %IGNOREINDEX SQLUser.nation.Nation_PK %IGNOREINDEX SQLUser.region.REGION_PK partsupp, supplier, nation, region where p_partkey = ps_partkey and s_suppkey = ps_suppkey and s_nationkey = n_nationkey and n_regionkey = r_regionkey and r_name = 'AFRICA' ... ``` *为什么要强制不用某些索引?* 一个是用在测试中,经常会比较不同索引的表现。比如你原来有个复合索引,它希望试试新创建的索引是不是更好, 那么很可能您需要告诉SQL引擎不要用以前的索引了。 还有就是您发现某个索引的使用没有让查询性能变好,强制不用它结果可以使用另一个索引,从而来得到更好的查询速度。 ## %ALLINDEX 用于测试所有可用的索引。 SQL引擎默认会在多个可用的索引中选中它判断最高效的,但这个判断不是总正确。加入%ALLINDEX会在生成查询计划前,测试所有可用的索引,以证实或者调整判断。 用到比较多的情况是有多个范围查询字句的情况。在Caché和早期IRIS版本中, 很多情况下, 使用%ALLINDEX会带来性能的提升, 尽管对所有可用索引做测试会有个额外开支. 比如以下的语句, ```sql SELECT TOP 5 ID, Name, Age, SSN FROM %ALLINDEX Sample.Person WHERE (:Name IS NULL or Name %STARTSWITH :Name) AND (:Age IS NULL or Age >= :Age) } ``` ## %NOINDEX 在最新版的IRIS文档中, 这个关键字已经去掉了。 我自己的测试中,在2022年后的IRIS中, 它其实已经不起作用了。 但在Caché中, 非常多的使用%NOINDEX的例子。 [Caché在线文档中的这段](https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_optquery#GSQLOPT_optquery_altshowplans)是这么说的:当绝大多数数据被条件选中(或未被选中)时,这种方法最常用。在小于 () 条件语句下,使用 %NOINDEX 条件级提示通常是有益的。对于“等于”条件语句,使用 %NOINDEX 条件级提示没有任何好处。对于连接条件语句,不支持在 =* 和 *= WHERE 子句外部连接中使用 %NOINDEX;而在 ON 子句连接中使用 %NOINDEX。 这是文档上的例子: E.Age
文章
Jingwei Wang · 二月 15, 2024

使用嵌入式 Python 和 OpenAI API 在 IRIS 中进行数据标签

大型语言模型(例如 OpenAI 的 GPT-4)的发明和普及掀起了一波创新解决方案浪潮,这些解决方案可以利用大量非结构化数据,在此之前,人工处理这些数据是不切实际的,甚至是不可能的。此类应用程序可能包括数据检索(请参阅 Don Woodlock 的 ML301 课程,了解检索增强生成的精彩介绍)、情感分析,甚至完全自主的 AI 代理等! 在本文中,我想演示如何使用 IRIS 的嵌入式 Python 功能直接与 Python OpenAI 库交互,方法是构建一个简单的数据标记应用程序,该应用程序将自动为我们插入IRIS 表中的记录分配关键字。然后,这些关键字可用于搜索和分类数据,以及用于数据分析目的。我将使用客户对产品的评论作为示例用例。 先决条件 运行的IRIS实例 OpenAI API 密钥(您可以在此处创建) 配置好的开发环境(本文将使用VS Code ) Review类 让我们首先创建一个 ObjectScript 类,该类将定义客户评论的数据模型。为了简单起见,我们将只定义 4 个 %String 字段:客户姓名、产品名称、评论正文以及我们将生成的关键字。该类应该扩展%Persistent,以便我们可以将其对象保存到磁盘。 Class DataTagging.Review Extends %Persistent { Property Name As %String(MAXLEN = 50) [ Required ]; Property Product As %String(MAXLEN = 50) [ Required ]; Property ReviewBody As %String(MAXLEN = 300) [ Required ]; Property Keywords As %String(MAXLEN = 300) [ SqlComputed, SqlComputeOnChange = ReviewBody ]; } 由于我们希望在插入或更新 ReviewBody 属性时自动计算 Keywords属性,因此我将其标记为SqlComputed。您可以在此处了解有关计算值的更多信息。 KeywordsComputation方法 我们现在想要定义一种方法,用于根据ReviewBody计算Keywords。我们可以使用Embedded Python直接与官方的openai Python包进行交互。但首先,我们需要安装它。为此,请运行以下 shell 命令: <your-IRIS-installation-path>/bin/irispip install --target <your-IRIS-installation-path>/Mgr/python openai 我们现在可以使用 OpenAI 的聊天完成 API 来生成关键字: ClassMethod KeywordsComputation(cols As %Library.PropertyHelper) As %String [ Language = python ] { ''' This method is used to compute the value of the Keywords property by calling the OpenAI API to generate a list of keywords based on the review body. ''' from openai import OpenAI client = OpenAI( # Defaults to os.environ.get("OPENAI_API_KEY") api_key="<your-api-key>", ) # Set the prompt; use few-shot learning to give examples of the desired output user_prompt = "Generate a list of keywords that summarize the content of a customer review of a product. " \ + "Output a JSON array of strings.\n\n" \ + "Excellent watch. I got the blue version and love the color. The battery life could've been better though.\n\nKeywords:\n" \ + "[\"Color\", \"Battery\"]\n\n" \ + "Ordered the shoes. The delivery was quick and the quality of the material is terrific!.\n\nKeywords:\n" \ + "[\"Delivery\", \"Quality\", \"Material\"]\n\n" \ + cols.getfield("ReviewBody") + "\n\nKeywords:" # Call the OpenAI API to generate the keywords chat_completion = client.chat.completions.create( model="gpt-4", # Change this to use a different model messages=[ { "role": "user", "content": user_prompt } ], temperature=0.5, # Controls how "creative" the model is max_tokens=1024, # Controls the maximum number of tokens to generate ) # Return the array of keywords as a JSON string return chat_completion.choices[0].message.content } 请注意,在提示中,我首先指定了我希望 GPT-4 如何“生成总结产品客户评论内容的关键字列表”的一般说明,然后给出两个示例输入以及所需的输入输出。然后,我插入 cols.getfield("ReviewBody") 并以“Keywords:”一词结束提示,通过提供与我给出的示例格式相同的关键字来推动它完成句子。这是Few-Shot Prompting技术的一个简单示例。 为了演示的简单性,我选择将关键字存储为 JSON 字符串;在生产中存储它们的更好方法可能是DynamicArray ,但我将把它作为练习留给读者。 关键词生成 现在,我们可以通过管理门户使用以下 SQL 脚本向表中插入一行来测试我们的数据标记应用程序: INSERT INTO DataTagging.Review (Name, Product, ReviewBody) VALUES ('Ivan', 'BMW 330i', 'Solid car overall. Had some engine problems but got everything fixed under the warranty.') 如下所示,它自动为我们生成了四个关键字。做得好! 结论 总而言之,InterSystems IRIS 嵌入 Python 的能力在处理非结构化数据时提供了多种可能性。利用 OpenAI 的强大功能进行自动数据标记只是利用这一强大功能可以实现的目标之一。这可以减少人为错误并提高整体效率。
文章
Kelly Huang · 九月 3, 2023

在 Python 上使用IRIS REST API 进行 SQL 迁移

对于即将到来的Python 竞赛,我想制作一个小型演示,介绍如何使用 Python 创建一个简单的 REST 应用程序,该应用程序将使用 IRIS 作为数据库。使用这个工具 FastAPI框架,高性能,易学,快速编码,可用于生产 SQLAlchemy 是 Python SQL 工具包和对象关系映射器,为应用程序开发人员提供 SQL 的全部功能和灵活性 Alembic 是一个轻量级数据库迁移工具,可与 SQLAlchemy Database Toolkit for Python 一起使用。 Uvicorn 是 Python 的 ASGI Web 服务器实现。 准备环境 假设已经安装了Python,至少是3.7版本。创建一个项目文件夹,并在其中创建一个包含以下内容的requirements.txt文件 fastapi== 0.101 .1 alembic== 1.11 .1 uvicorn== 0.22 .0 sqlalchemy== 2.0 .20 sqlalchemy-iris== 0.10 .5 我建议在 python 中使用虚拟环境,让我们创建新环境并激活它 python -m venv env && source env/bin/activate 现在我们可以安装我们的依赖项 pip install -r requirements.txt 快速启动 让我们使用 FastAPI 创建最简单的 REST Api。为此,请创建app/main.py from fastapi import FastAPI app = FastAPI( title= 'TODO Application' , version= '1.0.0' , ) @app.get("/ping") async def pong () : return { "ping" : "pong!" } 此时就足以启动我们的应用程序,并且它应该已经可以工作了。要启动服务器,我们将使用uvicorn -----0----- 我们可以发出ping请求 -----1----- FastAPI 提供 UI,我们可以在其中测试 API。 Docker化环境 要将 IRIS 添加到我们的应用程序中,我们将使用容器。 IRIS 镜像将按原样运行,但我们需要为 python 应用程序构建 Docker 镜像。我们需要Dockerfile FROM python: 3.11 -slim-buster WORKDIR /usr/src/app RUN --mount= type = bind ,src=.,dst=. \ pip install --upgrade pip && \ pip install -r requirements.txt COPY . . ENTRYPOINT [ "/usr/src/app/entrypoint.sh" ] 要在容器内启动应用程序需要一个简单的entrypoint.sh #!/bin/sh # Run SQL Migrations, to make DB Schema up to date alembic upgrade head # Start Python application uvicorn app.main:app \ --workers 1 \ --host 0.0.0.0 \ --port 8000 " $@ " 不要忘记添加执行标志 chmod +x entrypoint.sh 并在docker-compose.yml中与 IRIS 结合。 version: "3" services: iris: image: intersystemsdc/iris-community ports: - 1972 environment: - IRISUSERNAME=demo - IRISPASSWORD=demo healthcheck: test: /irisHealth.sh interval: 5 s app: build: . ports: - 8000 :8000 environment: - DATABASE_URL=iris://demo:demo@iris:1972/USER volumes: - ./:/usr/src/app depends_on: iris: condition: service_healthy command: - --reload 让我们来构建它 docker-compose build 第一个数据模型 现在让我们向应用程序声明对 IRIS 数据库的访问权限,添加文件app/db.py ,这将配置 SQLAlchemy 来访问我们的数据库,该数据库是通过 docker-compose.yml 传递的 URL 定义的。它包含几个处理程序,我们稍后将在应用程序中使用 import os from sqlalchemy import create_engine from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base from sqlalchemy.orm import sessionmaker DATABASE_URL = os.environ.get( "DATABASE_URL" ) if not DATABASE_URL: DATABASE_URL = "iris://demo:demo@localhost:1972/USER" engine = create_engine(DATABASE_URL, echo= True , future= True ) Base: DeclarativeMeta = declarative_base() SessionLocal = sessionmaker(autocommit= False , autoflush= False , bind=engine) def init_db () : engine.connect() def get_session () : session = SessionLocal() yield session 准备好定义我们应用程序中的第一个也是唯一一个模型。创建并编辑文件app/models.py ,它将使用 SQLAlchemy 定义模型,名为Todo ,包含三列: id、title 和 description 。 from sqlalchemy import Column, Integer, String, Text from app.db import Base class Todo (Base) : __tablename__ = 'todo' id = Column(Integer, primary_key= True , index= True ) title = Column(String( 200 ), index= True , nullable= False ) description = Column(Text, nullable= False ) 准备 SQL 迁移 在不断变化的世界中,我们知道我们的应用程序将来会得到改进,我们的表结构不是最终的,我们可以添加更多的表、列、索引等。在这种情况下,最好的方案是使用一些 SQL 迁移工具,它们有助于根据我们应用程序的版本升级数据库中的实际结构,并且使用这些工具,也有助于降级它,以防出现问题。虽然在这个项目中我们使用 Python 和 SQLAlchemy,但 SQLAlchemy 的作者提供了他的名为Alembic的工具,我们将在这里使用它。 我们需要使用我们的应用程序启动 IRIS 和容器,此时我们需要 bash,以便能够运行命令 -----2----- 运行命令alembic init app/migrations -----3----- 这准备好了 alembic 配置,我们需要修复它以满足我们的应用程序需求。为此,请编辑app/migrations/env.py文件。这只是文件的开头,应该更新,重点更新sqlalchemy.url和target_metadata 。下面的内容保持不变 import os import urllib.parse from logging.config import fileConfig from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config DATABASE_URL = os.environ.get( "DATABASE_URL" ) decoded_uri = urllib.parse.unquote(DATABASE_URL) config.set_main_option( "sqlalchemy.url" , decoded_uri) # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None : fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support from app.models import Base target_metadata = Base.metadata # target_metadata = None 我们已经有了一个模型,现在我们需要使用命令alembic revision --autogenerate创建一个迁移 -----4----- 让我们看看生成的迁移 上面说它找到了一个新的表todo,以及所有索引,并生成了一个文件,我们可以看一下这个文件,不需要编辑它,但是我们可以看到,它包含了升级和降级的功能。 """empty message Revision ID: 1e4d3b4d51ca Revises: Create Date: 2023-08-22 07:08:01.586330 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '1e4d3b4d51ca' down_revision = None branch_labels = None depends_on = None def upgrade () -> None : # ### commands auto generated by Alembic - please adjust! ### op.create_table( 'todo' , sa.Column( 'id' , sa.Integer(), nullable= False ), sa.Column( 'title' , sa.String(length= 200 ), nullable= False ), sa.Column( 'description' , sa.Text(), nullable= False ), sa.PrimaryKeyConstraint( 'id' ) ) op.create_index(op.f( 'ix_todo_id' ), 'todo' , [ 'id' ], unique= False ) op.create_index(op.f( 'ix_todo_title' ), 'todo' , [ 'title' ], unique= False ) # ### end Alembic commands ### def downgrade () -> None : # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f( 'ix_todo_title' ), table_name= 'todo' ) op.drop_index(op.f( 'ix_todo_id' ), table_name= 'todo' ) op.drop_table( 'todo' ) # ### end Alembic commands ### 现在是时候将其应用到数据库了,使用命令alembic Upgrade head ,其中 head 是升级到最新版本的关键字 -----5----- 降级 如果在升级应用过程中我们发现我们必须返回,我们可以降级数据库,例如1最后的修订版,将是head-1 -----6----- 并完全降级回空状态,使用关键字base 随时检查当前状态,如果缺少某些迁移将给出信息 -----7----- 使数据可访问 所以,我们现在可以回到 REST,我们需要它启动并运行,从当前容器退出并正常运行应用程序服务,uvicorn 有一个标志 --reload,所以,它将检查 python 中的更改文件,并在我们更改它们时重新启动 -----8----- FastAPI 使用 pydantic 项目来声明数据模式,我们也需要它,让我们创建app/schemas.py ,与 models.py 中的列相同,但采用简单的 Python 形式 from pydantic import BaseModel class TodoCreate (BaseModel) : title: str description: str class Todo (TodoCreate) : id: int class Config : from_attributes = True 在app/crud.py中声明增删改查操作,我们在其中使用 SQLAlchemy ORM 处理数据库 from sqlalchemy.orm import Session from . import models, schemas def get_todos (db: Session, skip: int = 0 , limit: int = 100 ) : return db.query(models.Todo).offset(skip).limit(limit).all() def create_todo (db: Session, todo: schemas.TodoCreate) : db_todo = models.Todo(**todo.dict()) db.add(db_todo) db.commit() db.refresh(db_todo) return db_todo 最后,我们可以更新app/main.py ,并添加读取和创建待办事项的路由 from fastapi import FastAPI, Depends from .db import init_db, get_session from . import crud, schemas app = FastAPI( title= 'TODO Application' , version= '1.0.0' , ) @app.on_event("startup") def on_startup () : init_db() @app.get("/ping") async def pong () : return { "ping" : "pong!" } @app.get("/todo", response_model=list[schemas.Todo]) async def read_todos (skip: int = 0 , limit: int = 100 , session=Depends (get_session) ) : todos = crud.get_todos(session, skip=skip, limit=limit) return todos @app.post("/todo", response_model=schemas.Todo) async def create_todo (todo: schemas.TodoCreate, session=Depends (get_session) ) : return crud.create_todo(db=session, todo=todo) 文档页面已相应更新,现在我们可以使用它了 试一试 添加新待办事项 并检查我们那里有什么 让我们在 IRIS 中检查一下 -----9----- 希望您在创建 REST 时享受使用 Python 和 FastAPI 的轻松体验。该项目的源代码可在 github 上找到:https://github.com/caretdev/fastapi-iris-demo 原贴作者:@ Dmitry Maslennikov
文章
Kelly Huang · 九月 3, 2023

独立模式下 EMPI 的安装和适配 - FHIR之转换和摄取

大家好。 在上一篇文章中,我们了解了如何配置 EMPI 来接收 FHIR 消息。为此,我们安装了 InterSystems 提供的 FHIR 适配器,该适配器配置了一个可以向其发送 FHIR 消息的 REST 端点。然后,我们将获取消息并将其转换为 %String,我们将通过 TCP 将其发送到 HSPIDATA 命名空间中配置的 EMPI 的输出。 好吧,是时候看看我们如何检索消息、将其转换回 %DynamicObject 并将其解析为 EMPI 用来存储信息的类。 TCP消息接收 正如我们所指出的,从配置了 FHIR 资源接收的生产中,我们已将消息发送到我们有业务服务侦听的特定 TCP 端口,在我们的例子中,该业务服务将是一个简单的EnsLib.TCP。 PassthroughService的目标是捕获消息并将其转发到业务流程,我们将在其中执行所需的数据转换。 这里有我们的商业服务: 这是它的基本配置: FHIR 消息的转变 正如你所看到的,我们只配置了通过 TCP 接收消息的端口以及我们将向其发送消息的组件,在我们的例子中我们将其称为 Local.BP.FHIRProcess,让我们看一下说类来看看我们如何从 FHIR 资源中检索信息: Class Local.BP.FHIRProcess Extends Ens.BusinessProcess [ ClassType = persistent ] { Method OnRequest(pRequest As Ens.StreamContainer, Output pResponse As Ens.Response) As %Status { set tDynObj = {}. %FromJSON (pRequest.Stream) If (tDynObj '= "" ) { set hubRequest = ##class (HS.Message.AddUpdateHubRequest). %New () // Create AddUpdateHub Message // Name, sex, DOB set givenIter = tDynObj.name. %Get ( 0 ).given. %GetIterator () while givenIter. %GetNext (, .givenName){ if (hubRequest.FirstName '= "" ) { Set hubRequest.FirstName=givenName } else { Set hubRequest.FirstName=hubRequest.FirstName_ " " _givenName } } Set hubRequest.FirstName=tDynObj.name. %Get ( 0 ).given. %Get ( 0 ) Set hubRequest.LastName=tDynObj.name. %Get ( 0 ).family Set hubRequest.Sex=tDynObj.gender Set hubRequest.DOB=hubRequest.DOBDisplayToLogical(tDynObj.birthDate) // Inserts full birth name information for the patient set nameIter = tDynObj.name. %GetIterator () while nameIter. %GetNext (, .name){ Set tName = ##class (HS.Types.PersonName). %New () if (name.prefix '= "" ) { Set tName.Prefix = name.prefix. %Get ( 0 ) } Set tName.Given = name.given. %Get ( 0 ) Set tName.Middle = "" Set tName.Family = name.family Set tName.Suffix = "" Set tName.Type= ^Ens .LookupTable( "TypeOfName" ,name. use ) Do hubRequest.Names.Insert(tName) } set identIter = tDynObj.identifier. %GetIterator () while identIter. %GetNext (, .identifier){ if (identifier.type'= "" ){ if (identifier.type.coding. %Get ( 0 ).code = "MR" ) { Set hubRequest.MRN = identifier.value Set hubRequest.AssigningAuthority = ^Ens .LookupTable( "hospital" ,identifier.system) Set hubRequest.Facility = ^Ens .LookupTable( "hospital" ,identifier.system) } elseif (identifier.type.coding. %Get ( 0 ).code = "SS" ) { Set hubRequest.SSN = identifier.value } else { Set tIdent= ##class (HS.Types.Identifier). %New () Set tIdent.Root = identifier.system // refers to an Assigning Authority entry in the OID Registry Set tIdent.Extension = identifier.value Set tIdent.AssigningAuthorityName = identifier.system Set tIdent. Use = identifier.type.coding. %Get ( 0 ).code Do hubRequest.Identifiers.Insert(tIdent) } } } // Address set addressIter = tDynObj.address. %GetIterator () while addressIter. %GetNext (, .address){ Set addr= ##class (HS.Types.Address). %New () Set addr.City=address.city Set addr.State=address.state Set addr.Country=address.country Set addr.StreetLine=address.line. %Get ( 0 ) Do hubRequest.Addresses.Insert(addr) } //Telephone set identTel = tDynObj.telecom. %GetIterator () while identTel. %GetNext (, .telecom){ if (telecom.system = "phone" ) { Set tel= ##class (HS.Types.Telecom). %New () Set tel.PhoneNumber=telecom.value Do hubRequest.Telecoms.Insert(tel) } } } Set tSC = ..SendRequestSync ( "HS.Hub.MPI.Manager" , hubRequest, .pResponse) Quit tSC } Storage Default { <Type> %Storage.Persistent </Type> } } 让我们更详细地看看我们正在做什么: 首先我们收到了业务服务发来的消息: Method OnRequest(pRequest As Ens.StreamContainer, Output pResponse As Ens.Response) As %Status { set tDynObj = {}. %FromJSON (pRequest.Stream) 正如我们在 OnRequest 方法的签名中看到的,输入消息对应于Ens.StreamContainer类型的类。 %String 类型消息的这种转换已在业务服务中进行。在该方法的第一行中,我们要做的是检索在 pRequest 变量中作为 Stream 找到的消息。然后,我们使用 %FromJSON 语句将其转换为 %DynamicObject。 通过将消息映射到动态对象,我们将能够访问已发送的 FHIR 资源的每个字段: set tDynObj = {}. %FromJSON (pRequest.Stream) If (tDynObj '= "" ) { set hubRequest = ##class (HS.Message.AddUpdateHubRequest). %New () // Create AddUpdateHub Message // Name, sex, DOB set givenIter = tDynObj.name. %Get ( 0 ).given. %GetIterator () while givenIter. %GetNext (, .givenName){ if (hubRequest.FirstName '= "" ) { Set hubRequest.FirstName=givenName } else { Set hubRequest.FirstName=hubRequest.FirstName_ " " _givenName } } Set hubRequest.FirstName=tDynObj.name. %Get ( 0 ).given. %Get ( 0 ) Set hubRequest.LastName=tDynObj.name. %Get ( 0 ).family Set hubRequest.Sex=tDynObj.gender Set hubRequest.DOB=hubRequest.DOBDisplayToLogical(tDynObj.birthDate) 在此片段中,我们看到如何创建HS.Message.AddUpdateHubRequest类的对象,该对象是我们将发送到负责在 EMPI 内执行相应操作的业务操作 HS.Hub.MPI.Manager 的对象,无论是是创建新患者或更新它,以及将其与 EMPI 中已有的其他患者可能存在的可能匹配项链接起来。 下一步是使用从业务服务接收到的数据填充新对象。正如您所看到的,我们所做的就是从刚刚创建的动态对象的不同字段中检索数据。动态对象的格式与 HL7 FHIR 为患者资源定义的格式完全对应,您可以直接在HL7 FHIR 网页上查看示例 对于我们的示例,我们从 HL7 FHIR 页面本身提供的列表中选择了该患者: { "resourceType" : "Patient" , "id" : "example" , "text" : { "status" : "generated" , "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n\t\t\t<table>\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Name</td>\n\t\t\t\t\t\t<td>Peter James \n <b>Chalmers</b> ("Jim")\n </td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Address</td>\n\t\t\t\t\t\t<td>534 Erewhon, Pleasantville, Vic, 3999</td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Contacts</td>\n\t\t\t\t\t\t<td>Home: unknown. Work: (03) 5555 6473</td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Id</td>\n\t\t\t\t\t\t<td>MRN: 12345 (Acme Healthcare)</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>" }, "identifier" : [ { "use" : "usual" , "type" : { "coding" : [ { "system" : "http://terminology.hl7.org/CodeSystem/v2-0203" , "code" : "MR" } ] }, "system" : "urn:oid:1.2.36.146.595.217.0.1" , "value" : "12345" , "period" : { "start" : "2001-05-06" }, "assigner" : { "display" : "Acme Healthcare" } } ], "active" : true , "name" : [ { "use" : "official" , "family" : "Chalmers" , "given" : [ "Peter" , "James" ] }, { "use" : "usual" , "given" : [ "Jim" ] }, { "use" : "maiden" , "family" : "Windsor" , "given" : [ "Peter" , "James" ], "period" : { "end" : "2002" } } ], "telecom" : [ { "use" : "home" }, { "system" : "phone" , "value" : "(03) 5555 6473" , "use" : "work" , "rank" : 1 }, { "system" : "phone" , "value" : "(03) 3410 5613" , "use" : "mobile" , "rank" : 2 }, { "system" : "phone" , "value" : "(03) 5555 8834" , "use" : "old" , "period" : { "end" : "2014" } } ], "gender" : "male" , "birthDate" : "1974-12-25" , "_birthDate" : { "extension" : [ { "url" : "http://hl7.org/fhir/StructureDefinition/patient-birthTime" , "valueDateTime" : "1974-12-25T14:35:45-05:00" } ] }, "deceasedBoolean" : false , "address" : [ { "use" : "home" , "type" : "both" , "text" : "534 Erewhon St PeasantVille, Rainbow, Vic 3999" , "line" : [ "534 Erewhon St" ], "city" : "PleasantVille" , "district" : "Rainbow" , "state" : "Vic" , "postalCode" : "3999" , "period" : { "start" : "1974-12-25" } } ], "contact" : [ { "relationship" : [ { "coding" : [ { "system" : "http://terminology.hl7.org/CodeSystem/v2-0131" , "code" : "N" } ] } ], "name" : { "family" : "du Marché" , "_family" : { "extension" : [ { "url" : "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix" , "valueString" : "VV" } ] }, "given" : [ "Bénédicte" ] }, "telecom" : [ { "system" : "phone" , "value" : "+33 (237) 998327" } ], "address" : { "use" : "home" , "type" : "both" , "line" : [ "534 Erewhon St" ], "city" : "PleasantVille" , "district" : "Rainbow" , "state" : "Vic" , "postalCode" : "3999" , "period" : { "start" : "1974-12-25" } }, "gender" : "female" , "period" : { "start" : "2012" } } ], "managingOrganization" : { "reference" : "Organization/1" } } 首先,我们创建了 2 个查找表,用于映射姓名类型和分配医疗记录号 (MR) 的权限,第一个与 EMPI 管理的类型兼容,第二个用于识别分配权限生成标识符: Set tName.Type= ^Ens .LookupTable( "TypeOfName" ,name. use ) Set hubRequest.AssigningAuthority = ^Ens .LookupTable( "hospital" ,identifier.system) Set hubRequest.Facility = ^Ens .LookupTable( "hospital" ,identifier.system) 启动测试消息 完美,让我们针对我们在上一篇文章中定义的端点启动 FHIR 消息: 正如您所看到的,我们收到了 200 响应,这仅意味着 EMPI 已正确接收到消息,现在让我们看看在我们的生产中生成的跟踪: 这里我们有我们的病人,您可以看到转换已成功执行,并且 FHIR 消息中报告的所有字段都已正确分配。可以看到,一条IDUpdateNotificationRequest通知消息已经生成了。当在系统中执行创建或更新患者的操作时,会生成此类通知。 很好,让我们通过按姓名搜索患者来检查患者是否在我们的系统中正确注册: 答对了!让我们更详细地看看我们亲爱的Peter的数据: 相当完美!我们的 EMPI 中已经包含了有关患者的所有必要信息。正如您所看到的,该机制非常简单,让我们回顾一下我们执行的步骤: 我们已将 InterSystems 提供的 FHIR 适配器工具安装在配置为支持互操作性的命名空间(与 EMPI 独立安装生成的命名空间不同的命名空间,在我的例子中称为 WEBINAR)中。 我们在此命名空间中创建了一个业务操作,它将接收到的HS.FHIRServer.Interop.Request类型的消息转换为 %String,并将其发送到在 EMPI 命名空间 (HSPIDATA) 的生产中配置的业务服务。 接下来,我们添加了EnsLib.TCP.PassthroughService类的业务服务,该类接收从 WEBINAR 命名空间的生成发送的消息并重定向到业务流程Local.BP.FHIRProcess 。 在 BP Local.BP.FHIRProcess 中,我们已将接收到的 Stream 转换为HS.Message.AddUpdateHubRequest类型的对象,并将其发送到业务运营HS.Hub.MPI.Manager ,该管理器将负责将其注册到我们的EMPI。 正如您所看到的,EMPI 功能与 IRIS 集成引擎提供的功能的结合使我们能够使用几乎任何类型的技术。 我希望这篇文章对您有用。如果您有任何问题或建议,您已经知道,请发表评论,我将很乐意为您解答。 原贴作者:@Luis Angel
文章
Nicky Zhu · 十月 10, 2024

FHIRValidation - 用IRIS验证你自己的FHIR IG

本演示程序用于展示如何采用自定义FHIR profile来验证数据合规性。自定义FHIR实施指南基于[FHIR R4版本](https://hl7.org/fhir/R4/index.html)开发,在本例中实现了对[Organization](https://hl7.org/fhir/R4/organization.html)资源的扩展并用于验证数据的合规性。 # 安装 1. 通过Git clone下载本项目。 2. 执行docker-compose up -d构建并启动容器,初次执行时需执行需10~15分钟(视配置变化)。将构建InterSystems IRIS for Health镜像,安装FHIR服务器,导入自定义FHIR规范,使自定义FHIR 规范可用于验证数据。 3. 在Postman中导入TestCases中的测试用例文件,查看各类FHIR约束的测试效果 4. 容器启动后可查看[自定义IG](http://localhost:52880/csp/FullIG/index.html)内容 # 项目代码结构 ``` FHIRValidation ├─ ExampleIG │ ├─ ig.ini │ ├─ input │ │ ├─ fsh │ │ │ ├─ alias.fsh │ │ │ ├─ codesystems.fsh │ │ │ ├─ organization.fsh │ │ │ └─ valuesets.fsh │ │ └─ pagecontent │ │ └─ index.md │ └─ sushi-config.yaml ├─ README.md ├─ README_zh.md ├─ TestCases │ └─ FHIR Profile-based Validation testcases.postman_collection.json ├─ docker-compose.yml └─ image-iris ├─ Dockerfile └─ src ├─ FullIG ├─ IGPackages │ ├─ hl7.fhir.uv.extensions.r4#5.1.0.tgz │ ├─ hl7.terminology.r4#6.0.2.tgz │ └─ package.tgz └─ init └─ init.sh ``` ## ExampleIG 该子目录下的所有文件为本项目所采用的自定义FHIR规范[SUSHI](https://fshschool.org/docs/sushi/)源码,供用户定义FHIR规约时参考使用。 ## TestCases 该子目录下存放基于FHIR REST API的测试用例脚本,需导入到Postman中使用 ## image-iris 该子目录下存放nterSystems IRIS for Health镜像所需的文件,其中: └─ src ├─ FullIG 该目录中存放SUSHI生成的自定义FHIR IG ├─ IGPackages 该目录中存放自定义FHIR IG的 [package](#fhir-package) 文件 └─ init 该目录中存放IRIS的Docker镜像初始化脚本 # FHIR package简介 HL7组织推荐使用实施指南([Implementation Guild](https://build.fhir.org/ig/FHIR/ig-guidance/))来解释如何使用FHIR规范。除用于开发人员阅读的说明(如html)外,实施指南中通常也包括可直接被机器读取和应用的工件(artifacts),可被用于驱动代码生成和数据验证等任务。 FHIR实施指南采用[NPM Package](https://docs.npmjs.com/cli/v8/configuring-npm/package-json)规范管理依赖。指南涉及的所有StructureDefinition,ValueSet等资源将被打包在一块,形成可被FHIR Server用于读取规范,生成客户端代码或执行数据质量校验的资源包。 通过SUSHI工具生成的实施指南中就包含若干package文件。如本例中,image-iris/src/IGPackages/package.tgz即为生成的package包,可被IRIS FHIR Server直接导入使用。应当注意到的是,除核心资源包(如[hl7.fhir.r4.core](https://hl7.org/fhir/R4/downloads.html))外,完整的FHIR规范还需要引用术语、扩展等额外的资源包。 目前FHIR规范引用机制的文档尚不完善。如基于R4版的FHIR规范除引用hl7.fhir.r4.core外,还需引用[hl7.fhir.uv.extensions.r4#5.1.0](https://simplifier.net/packages/hl7.fhir.uv.extensions.r4/5.1.0)与[hl7.terminology.r4#6.0.2](https://terminology.hl7.org/downloads.html),但这些引用关系在[R5版本](https://hl7.org/fhir/packages.html)中方有记录,在R4版文档中并未完整声明,需开发者在开发过程中自行补充。 在本例中这些包已下载在image-iris/src/IGPackages文件夹下,将作为依赖在自定义FHIR实施指南之前加载。 # FHIR validation简介 参见FHIR规范[Validating Resources](https://hl7.org/fhir/R4/validation.html)一节。FHIR规范已经设计了对数据结构、属性基数、值域、代码绑定和约束等一系列机制在内的数据质量校验机制。HL7组织在FHIR规范中并未强制要求遵循何种强度的质量控制,但建议采用[宽进严出](https://hl7.org/fhir/R4/validation.html#correct-use)的原则处理FHIR数据。 对于保存FHIR资源的FHIR存储库而言,保障FHIR资源的数据质量是使医疗行业具有价值,保障医疗行为安全性的前提条件。因此,在构建基于FHIR存储的数据共享交换方案时,即使不得不保存不满足数据质量要求的数据,也应对其进行校验,标识不符合项,推动数据治理活动的进行,从而保障医疗安全和数据消费者的利益。 在FHIR规范指出的多种数据校验方式中,FHIR Validator和[FHIR操作](https://hl7.org/fhir/R4/resource-operation-validate.html)对数据质量校验的覆盖最为全面。 本例将使用InterSystems IRIS for Health所提供的`$`validate操作,通过profile参数对尚未保存的FHIR数据进行校验。使用者也可修改测试用例,构建HTTP POST参数,对存量FHIR资源进行校验。 还应当注意的是,$validate操作如被正确调用,将通过Http 200返回校验结果,如有不符合项,将在返回的OperationOutcome资源中包裹错误信息,而不通过Http代码标识错误。 # 对FHIR的扩展 在本例中基于FHIR R4对Organization资源进行了如下扩展: ## 1. 修改language的绑定强度 将机构主要语言的绑定强度修改为required ## 2. active字段基数从0..1改为1..1 从而使得数据的状态成为必填字段,有且只有一个元素 ## 3. name字段基数从0..1改为1..1 组织机构名称成为必填字段,有且只有一个元素。参考我国医院除医院名称外,如果具备急救中心、胸痛中心等牌照,还可能具有多个名称。但因注意到,这些牌照通常标识了医疗机构提供的服务能力,而非在组织机构注册系统中具备的法定名称,且此类牌照生命周期与医疗机构自身的生命周期并不一致。因此,从牌照获得的名称宜视为该医疗机构的服务能力而非机构的唯一名称。在FHIR中,通过服务能力获得的名称可通过资源HealthcareService提供,该资源与Organization资源间可建立多对一的引用关系,更适合用来表达上述概念。 ## 4. 增加医疗机构的组织机构类型 根据中国国家标准GB/T 20091-2021 组织机构类型,分别增加了CodeSystem organizationtype-code-system和ValueSet organizationtype-vs,并通过Extension向Organization资源中添加了扩展mdm-organizationTypeExtension,从而使得该资源可用于表示表示标识中国组织机构类型。 该扩展通过对Extension切片实现,且基数为1..1,即医疗机构资源必须具有组织机构类型元素。 ## 5. 约束医疗机构证件号码 FHIR基础标准并未纳入中国组织机构统一社会信用代码的证件类型,为此增加了CodeSystem cs-identifierType-code-system,并对Identifier按其类型进行了切片,使之必须可以表达社会信用代码。且社会信用代码的格式遵循以下约束: 1. identifier.use必须取值为official,即正式/官方用途 2. identifier.type必须遵循cs-identifierType-code-system要求,system必须为该codesystem的uri,code必须为“USCC” 3. identifier.value必须遵循自定义约束uscc-length-18,该字段长度必须为18位,其中前17位必须为数字,最后1位必须为数字或字母 # 测试用例列表 ## 1. Without profile - All OK 未声明资源对应的profile,因此FHIR Server将不对资源中各属性的值进行校验,仅返回All OK。 ## 2. Unknow field 在资源中加入了未被定义的属性isNational,因此校验引擎返回了Unrecognized element错误。 ## 3. Wrong cardinality - less 在本IG中,修改了Organization资源name属性的基数为1..1,即应有且仅有一个组织机构名称。本测试用例未填写名称,因此数据校验失败。 另外,可以观察到Identifier.type经过扩展,加入了统一社会信用代码作为标识符类型,FHIR R4规范里并不包含这个值,但该字段的代码绑定强度仅为example,不强制约束。因此校验引擎返回了information级的值域代码不符合信息而没有报错。 ## 4. Binding strength 在本IG中,组织机构的language属性的代码绑定强度改为了required,则该字段值域必须符合http://hl7.org/fhir/ValueSet/languages,因此,当该字段取值为wrong language时,因不在required值域中,将导致error级错误 ## 5. Wrong value 在本IG中,组织机构类型的值域来自于organizationtype-code-system,因此,当类型为mdm-organizationTypeExtension的extension元素中code的值为“999”,不在值域中时,将导致error级错误 ## 6. Failing invariant 在本IG中,组织机构的社会信用代码必须遵循自定义约束uscc-length-18(该字段长度必须为18位,其中前17位必须为数字,最后1位必须为数字或字母),因此,当其末位为字符“%”时,违反该约束,将导致error级错误 ## 7. Failing profile 对于一个资源定义的一个profile包含了多个约束,因此,在校验时所有不满足profile的问题都将被检出,例如本例中: 1. 错误的language代码 2. 错误的组织机构类型 3. 缺少name字段 可见上述问题都被检出