InterSystems开发者社区中文版第二届技术征文大赛!

Detailed rules

InterSystems开发者社区中文版第二届技术征文大赛!

嗨,开发者们!

秋高气爽之际,我们很高兴地宣布启动InterSystems开发者社区中文版第二届技术征文大赛

2023年9月19日-11月24日,欢迎热爱InterSystems技术的你来投稿,撰写与InterSystems技术相关的文章。

🎁参与奖 我们为每一位参与此次征文大赛的作者准备了礼品!

🏆优秀文章大奖 Apple AirPods Pro; Osprey Proxima Backpack; Home Office Pro Lap Desk; Sound Machine with Wireless Charger; JBL Pulse 5 Bluetooth Speaker; Sound Machine with Wireless Charger; Hammock InterSystems; Light Up Your Logo Charging Pad; InterSystems Developer Community Blanket

👉 点击了解更多详情
 

参赛要求

征文期间,发布在中文社区的文章只要满足以下要求,将自动参加比赛,无需额外提交:

  • 文章必须与InterSystems技术有关
  • 文章必须以中文撰写
  • 文章必须是100%的原创文章(可以是现有文章的延续)
  • 文章应在InterSystems开发者中文社区首发,严禁从其他社区进行搬运
  • 文章严禁抄袭或翻译社区现有文章
  • 社区成员可以发布多篇文章参赛
  • 文章字数应不少于800字,写作时请关注编辑器右下角的计数器

奖品设置

1. 专家提名奖:活动期间发布文章且成功参赛后,由InterSystems专家评选得出

🥇第一名,Apple AirPods Pro(2nd Generation w Active Noise Cancellation)

🥈第二名,Osprey Proxima Backpack

🥉第三名,Home Office Pro Lap Desk

🏆第四名-第六名,Sound Machine with Wireless Charger

2. 开发者社区奖:活动期间发布文章且成功参赛后,由社区成员评选得出,“点赞”数前五名获得以下奖品

🥇第一名,JBL Pulse 5 Bluetooth Speaker

🥈第二名,Sound Machine with Wireless Charger

🥉第三名,Hammock InterSystems

🏆第四名,Light Up InterSystems Charging Pad

🏆第五名,InterSystems Developer Community Blanket

3. 入围奖:在征文大赛期间,所有在InterSystems开发者中文社区发布文章且成功参赛的其余用户都将获得特别奖励。

关于参赛规则等更多详情,请点击以下链接:

InterSystems开发者社区中文版第二届技术征文大赛公告

九月 19 - 十一月 23, 2023
按专家评分
专家: 23
喜欢:
文章 姚 鑫 · 十一月 16, 2023 17m read

浅谈一下个人基于IRIS后端业务开发框架的理解

现状

由于国内使用基于M语言IRIS平台几乎都在医疗行业。医疗系统又非常的庞大和复杂。前期由于快速占领市场,系统数量越来越多,到了临界点后就产生了质变,所以前期基于功能的线性开发注重效率,所以导致大量的产品业务代码有如下集中情况:

  • 系统交互乱如麻,各系统的交互关系变成了网状。
  • 系统规模庞大,内部耦合严重,牵一发而动全身,后续修改和扩展困难,开发效率低。
  • 关键功能逻辑复杂,容易出现问题,出现问题后很难排查和修复,开发成本高。
  • 功能越来越多,导致系统复杂度指数级上升。
  • 重复造轮子,相似的功能不断重复开发。

image

如上图所示,这仅仅是展示了五个模块之前的交互,在此基础上继续增加模块则复杂度成指数级上升,并且如果每个模块之间如果没有做好接口管理,维护起来也是地狱级别。

如何解决这个问题,一般会分为两派:优化派与架构派。

优化派的思想是将现有的系统优化。例如重构某个方法,优化某个SQL。优化派的优势是针对系统改动较小,可以保持系统的稳定性,可以快速实施,缺点是治标不治本,随着需求越来越多,增加的代码量越多,后期还是会撑不住。

架构派的核心思想是调整系统架构,将原来的大系统拆分为多个互相配合的小系统,例如药库系统拆分为,采购模块,订单模块,查询模块,分析模块等。架构派的优势是一次调整可以支撑比较长期的业务发展,缺点是动作较大,耗时较长,稳定性需要项目考验。

所以这是个长痛短痛的问题,相信很多公司在遇到这种情况时,大部分情况下优化派会赢,因为“优化”是最快、最稳定的方式。除非当前架构支撑不了当前业务的阶段,才会考虑重新架构。


复杂性的一个主要原因就是系统越来越庞大,业务越来越多。降低复杂性最好的方式就是“拆”,化整为零,分而治之,将整体复杂性分散到多个子业务或子系统里去。

基于此我们设计一个简单的接口隔离方案,模块之间交互一般都会抽离个接口类,使用中介者模式或门面模式思想设计:

  • 门面模式:

每个模块之间,有单独的门面来专门处理接口,有效防止接口散布在模块内部的类中,起到接口隔离作用。

image

  • 中介者模式:

因为模块之间交互频繁,随着模块的增加,接口越来越多,会导致中介者类越来越庞大。好处是可以统一做接口管理。

image

这里的门面类可以进步一划分为提供接口与调用接口,可以类比为读写分离。

image

以上是处理多模块之间的常规交互处理方案。


下面我想针对单独模块内部的功能业务进行一些探讨:

在现实环境中,我们不可能一开始就设计出一套完美的架构,都会随着业务的发展,暴露出的问题,在不断的的迭代中,保留优秀的设计,修复有缺陷的设计,逐渐完善。不断进行重构,所以没有一劳永逸的框架。

笔者也曾开发了一些产品,基于此有一些基于M语言IRIS后端的一些框架想法分享给大家。 热门的语言JavaPython等有各种成熟的框架。对比关于IRIS相关的开发框架也比较少。借助相关的框架学习其中的思想,设计出一套适合基于IRIS平台M开发语言的医疗行业系统的开发框架。 尽量把框架做到,适合、简单。

  • 结合设计模式思想。
  • 根据功能进行模块划分。
  • 进行接口隔离,让产品、甚至功能做到高内聚,低耦合,可插拔。

方案

无论何种方案和框架,几乎都是为了一个统一的目标,让业务模块之间的耦合度尽量的低,做到可插拔,相互之间不影响,包括微服务,SOA。或者一些常见的架构MVCMVP。其思想都是根据职责或业务划分成不同的模块。

我们以药库的,订单,采购,入库的业务功能为例,具体展示一下业务功能的一些划分。

image

下面我们就针对如上这张图每个模块类来进行讨论。

无论采取何种分层维度,分层架构设计最核心的一点就是需要保证各层之间的差异足够清晰,边界足够明显。否则如果两个层的差异不明显,就会出现程序员A认为某个功能应该放在A层,而程序员B认为同样的功能应该放在B层,这样就导致了分层混乱。如果这样的架构进入实际开发落地,则A层和B层就会乱成一锅粥,也就失去了分层的意义。

按照如上分层就能够较好地完成系统扩展,本质在于:隔离关注点。即每个层中的组件只会处理本层的逻辑。比如说,展示层只需要处理Api相关。业务层只需要处理业务逻辑。数据层只提供数据。SQL层只处理SQL。接口层只处理各种服务。这样我们拓展某层时,其他层是不受影响的,通过这种方式可以支持系统在某曾上的快读扩展。

按照上图功能划分,好处是在于将业务功能强制分层,依赖限定为两两依赖,降低了系统复杂度。但是分层结构的代码特点就是冗余,也就是说,不管这个业务有多么简单,每层都必须要参与处理,有时甚至调用一个接口,要通过类包装函数层层传递。

我们是否应该自由选择绕过分层的约束呢?答案是不建议这样做,因为在传统的代码当中,我们已经见识到当接口方法在各种类中相互调用,时间一长,架构就会变的混乱,牵一发动全身。除此之外,虽然分层架构的实现在某些场景下看起来有些烦琐和冗余,但复杂度却很低。也不会增加太多工作量。无非在类多添加几个引用。

这个方案的另外一个典型缺点就是性能,因为每一次业务功能请求有可能需要穿越多个接口层,多少都会有一些性能的浪费。当然,这里所谓的性能缺点只是理论上的分析,实际上带来的性能损失,可以忽略不计。有时了为了保证方法类接口的单一性,牺牲一些性能也是在所难免的,最常见的例子就是用Gloabl取数据比写SQL要快,但是SQL在语义上比GLoabl要明确一些,实际上0.000010.0001性能上的感知几乎没有。但这并也并非说写SQL一定比Global要好。要根据情况而定,例如复杂SQL有多个JOIN,这种性能肯定会很比Global要慢很多。

具体实现

借用MVP架构和微服务的思想,后台将按每个大模块进行划分,考虑数据抽离、可复用性、原子性、安全性等原则,模块内部建立不同类进行区分。

注:一旦遵守框架开发,需要严格执行,否则过不了多久,又会乱成一锅粥。在现实环境中,多人开发确实很难达到一致性。

Base

image

其中:

  • His.Common.Base - 作为整个医院业务模块最底层包,存放一些所有产品组都可以用的公方法或开关。例如:获取pid,获取院级锁方法,抽象出一写全局方法,编译自动备份方法等。
  • Pharmacy.Common.Base - 其中Pharmacy作为His单独模块的包名,例如医生Doctor、护士Nurse、药房Medicine。该模块可以实现产品组级公共方法,实现His级的抽象方法,例如产品组级的锁。模块级的公共参数,字典变量等。
  • Pharmacy.Stock.Base - 该层为产品组级下的产品线,例如药房包括药库Stock、门诊药房Outpatient等。该层可以实现产品线级的公共方法,实现Pharmacy.Common.Base的抽象方法等。例如产品线级的锁。
  • 其中所有的BASE模块都可以包含对应层级的公用方法、公用包、inc文件,属性、参数、常量。每一个模块继承自己的上一级Base类。

注:可以把共有算法抽象到父类,子类写具体的模块的细节、模版方法模式。

代码示例:

  • His.Common.Base
    • 导入了通用包。例如:SQL,Util
    • 全局的参数。例如:当前日期时间等。
    • 定义抽象锁方法提子类重写。
/// 导入公共表与工具类
Import (SQLUser, Util)
/// todo:引入院级inc文件
Include His.Common.Base


Class His.Common.Base Extends %RegisteredObject
{

/// todo:院级公共属性
/// todo:院级公共参数
/// todo:院级公共方法
Parameter sysDate = {+$h};

Parameter sysTime = {$p($h, ",", 2)};

ClassMethod NewPid() As %String
{
    q $i(^HisPid)
}

ClassMethod Lock(lockName, lockTime) [ CodeMode = expression ]
{
..AbstractLock(lockName, lockTime)
}

ClassMethod AbstractLock(lockName, lockTime) [ Abstract ]
{
}

ClassMethod Unlock(lockName) [ CodeMode = expression ]
{
..AbstractUnlock(lockName)
}

ClassMethod AbstractUnlock(lockName) [ Abstract ]
{
}

}


  • Pharmacy.Common.Base
    • 实现模块级公用方法。此处为药房模块。
Class Pharmacy.Common.Base Extends His.Common.Base
{

/// todo:产品组模块公共属性
/// todo:产品组模块公共参数
/// todo:产品组模块公共方法
ClassMethod AbstractLock(lockName, lockTime)
{
    l +^PharmacyLock(lockName):lockTime e  q $$$NO
    q $$$OK
}

ClassMethod AbstractUnlock(lockName)
{
  	l -^PharmacyLock(lockName)
    q 0
}

}

  • Pharmacy.Stock.Base
    • 实现产品级公用方法。药房下药库产品。
Class Pharmacy.Stock.Base Extends Pharmacy.Common.Base
{

/// todo:产品线模块公共属性
/// todo:产品线模块公共参数
/// todo:产品线模块公共方法
ClassMethod AbstractLock(lockName, lockTime)
{
    l +^PharmacyStockLock(lockName):lockTime e  q $$$NO
    q $$$OK
}

ClassMethod AbstractUnlock(lockName)
{
  	l -^PharmacyStockLock(lockName)
    q 0
}

}


  • His.Common.Base.inc
    • 全局通用的inc文件。关于inc文件的使用可以参考百讲宏的使用.
#define HIS "HIS"

Biz

传统方式里我们习惯按建立的表类来写业务,这种方式的问题是当有主子表时我们的业务逻辑过于分散,一般主子表的业务操作都是联动的。所以我们可以根据业务的属性来划分到一个Biz里,这样做提高了内聚的属性。

image

  • 实现业务的主要逻辑代码,此类中不应包含具体的SQL语句、详细的过滤条件、复杂算法等。
  • Pharmacy.Stock.Ord.Biz - 实现该药库下订单模块的业务逻辑。
  • Biz的方法,应按照六大原则来书写,单独的功能抽离方法,业务方法应该是单一原则的方法嵌套。
Class Pharmacy.Stock.Ord.Biz Extends Pharmacy.Stock.Base
{

/// 保存订单主表业务逻辑
ClassMethod SaveMain(params)
{
	//todo:过滤条件
	
	q:(##class(Filter).IsSave()) $$$ERROR($$$GeneralError, "没有保存")
	q:(##class(Filter).IsAuit()) $$$ERROR($$$GeneralError, "没有通过审核")
	q:(##class(Filter).IsExist()) $$$ERROR($$$GeneralError, "不存在")
	
	//todo: 插入sql
	s id = ##class(Sql).SaveMain()
	q id
}

/// 保存订单子表业务逻辑
ClassMethod SaveDetail(params)
{
	//todo:过滤条件
	q ##class(Sql).SaveDetail()
}

/// 返回多条数据
ClassMethod Query(params)
{
}

/// 返回单条数据
ClassMethod QueryById(params)
{
	q ##class(Data).GetMainData()
}

}


  • Pharmacy.Stock.Base - 药库基本都涉及到保存主子表所以可以根据自己产品的需求抽象出一些公用方法,供其他功能模块重写。
Class Pharmacy.Stock.Base Extends Pharmacy.Common.Base
{


/// 公共保存方法
ClassMethod Save(params)
{
	// todo:算法,过滤条件
	#; 保存主算
	s ret = ..SaveMain(params)
	// todo:算法,过滤条件
	#; 保存明细数据
	s ret = ..SaveDetail(params)
	q $$$OK
}

/// 抽象保存主表算法
ClassMethod SaveMain(params) [ Abstract ]
{
}

/// 抽象保存子表算法
ClassMethod SaveDetail(params) [ Abstract ]
{
}

}



Data

image

在传统的书写习惯里可以看到几乎每个索引查询都会单独写一遍取数据。而且取数据的查询方法散布了很多类里。在后期维护时非常费时费力。所以每个业务模块都应有自己的Data类,取数据时都在统一的Data类里去取,目的是提高数据复用性。

  • Biz类返回数据,是模块内获取详细数据的唯一类,数据以动态JSON形式传递,抽离取值、提高复用。

注意:动态JSON非常的高效方便。

代码示例:

  • Pharmacy.Stock.Ord.Data
Class Pharmacy.Stock.Ord.Data Extends %RegisteredObject
{

/// 获取主数据
ClassMethod GetMainData(id)
{
	s data = ^OrdD(id)
	
	s ret = {}
	s ret.no = $lg(data, 1)
	s ret.createUserName = $lg(data, 2)
	s ret.finished = "Y"
	q ret
}

/// 获取明细数据
ClassMethod GetDetailData(id)
{
	s data = ^OrdD(+id, "I", $p(id,"||",2))
	
	s ret = {}
	s ret.name = $lg(data, 1)
	s ret.code = $lg(data, 2)
	s ret.desc = $lg(data, 3)
	q ret
}

}

注意:这里一定要区分好哪些是业务模块数据,哪些是公用数据,如果是公用数据不要放到业务模块数据里,要提到产品级的公用数据,依次类推。

例如:取登录人名称,登录人代码,就要去上一级的公共数据取。

Pharmacy.Stock.Ord.Data 的上一级Pharmacy.Stock.Common.Data存放药库级的公用数据。这里不应该放入取登录人方法。应该再次往上找Pharmacy.Common.Data,这里存放药房公用数据。实际上这里也不应该存放登录人方法,但是实际上每个产品组都是互为独立,大多数都把取登录人的方法放到这里。实际上应该放到His.Common.Data

  • His.Common.Data
Class His.Common.Data Extends %RegisteredObject
{

/// desc: 获取登录用户ID
ClassMethod GetUserId()
{
	q:'$d(%session) ""
	q:'$d(%session.Data("login.user.id")) ""
	s data = %session.Data("login.user.id")
	q data
}

/// desc: 获取登录用户ID
ClassMethod GetUsername()
{
	q:'$d(%session) ""
	q:'$d(%session.Data("login.user.name")) ""
	s data = %session.Data("login.user.name")
	q data
}

}

注:取数据类包保持单一原则,做到取desccodeid等数据时。可以直接取数据,而不是取到id再去根据id取对应Global做转换。

当遇到方法不知道放到哪里时,应该咨询有经验的组里资深程序员。

Filter

image

在传统的书写代码中,几乎看不到为专门的过滤条件建立的类,每次写业务逻辑时,尤其在循环中过滤条件都会复写一次,在字段过滤中最为明显,每次先查表找到过滤字段的位置,再取过滤字段,然后判断该字段是否满足条件,满足则过滤。

这种方式的问题很明显,每次过滤时查表费时费力,字段位置容易出错,大量的重复判断代码,当数据库字段有变动时,每个地方都需要改。而且一个业务可能会出现多个过滤条件,写新业务时可能会遗漏过滤条件。其他产品需要判断过滤条件时无接口可提供,往往是通过口头的方式告知这个表哪个字段为Y时过滤,他人还得需要再取一次。

Biz类提供条件过滤,优先考虑返回布尔类型结果。类中方法要保持原子性(不可拆分),只拓展不修改,提高复用,复杂过滤条件通过的单个方法组合的形式实现。注意保持单一原则,相同条件只允许有一个。

Class Pharmacy.Stock.Ord.Filter Extends %RegisteredObject
{

/// desc:判断是否保存
ClassMethod IsSave(ID As %Integer) As %Boolean
{
  s compFlag = $p(^OrdD(ID ), "^", 4)  
  q:(compFlag = "Y") $$$YES
  q $$$NO
}

/// desc:判断是否审核
ClassMethod IsAuit(ID As %Integer) As %Boolean
{
  s aduitFlag = $p(^OrdD(ID ), "^", 5)  
  q:(aduitFlag = "Y") $$$YES
  q $$$NO
}

/// desc:  判断是否存在
ClassMethod IsExist(ID As %String) As %Boolean
{
  s id = $p(^OrdD(ID), "^", 6)  
  q:(id '= "") $$$YES
  q $$$NO
}

/// desc:  是否允许
ClassMethod IsAllow(ingr As %String) As %Boolean
{
  q:..IsSave(ingr) $$$YES
  q:..IsAuit(ingr) $$$YES
  q:..IsExist(ingr) $$$YES
  q $$$NO
}

}

通过过滤条件的组合的好处是。为某个业务提供过滤时,我们仅提供组合好的接口即可,例如入库复杂的业务,需要考虑很多条件。这里只需要提供一个组合好的过滤接口,方便高效。

注意:这里一定要区分好哪些过滤条件是功能级别、产品线级别、模块组级别、全院级别。

Sql

image

传统类中SQL语句,我们可以发现有很多的相同的业务SQL语句散落在各个类中,也可以看到不同的方法有相同的SQL语句,这种碎片化的SQL语句有很多,也非常不好管理。所以可以根据小三层的思想把SQL语句放到专门的SQL类里,这样既增强了SQL语句的复用,也方便管理该业务下的所有相关SQL语句。当有新功能时,可以一览所有已有SQL,避免重复开发。

如果想避免重复的大量的简单SQL,可以参考这篇文章 只需要改造一下实体类,以后再也不用写SQL了

注意:后续对于数据操作都将仅仅存在SQL一种形式,不再使用对象存储。

Class Pharmacy.Stock.Ord.Sql Extends %RegisteredObject
{

ClassMethod SaveMain(params)
{
	
	//todo: sql
}

ClassMethod SaveDetail(params)
{
	//todo: sql
}

ClassMethod Delete(params)
{
	
	//todo: sql
}

ClassMethod Update(params)
{
	
	//todo: sql
}

ClassMethod Insert(params)
{
	
	//todo: sql
}

}

ImpRef

image

我们这里把接口分为了提供接口Imp与引用接口Ref,顾名思义Imp为对外的的接口,Ref为对内引用的第三方接口。

这样做的好处是隔离了功能模块间的耦合度,也统一了接口管理。接口做到了功能模块级,对于功能模块可以做到单独部署,统一了内外部引用,借用了微服务思想。

否则在传统模式下接口可能会散落在各个类中,接口级的管理顶多做到了产品组级。功能模块内部耦合严重。

模块向其他模块提供访问的接口类,所有业务模块之间的访问都应通过ImpRef类访问,模块间应保持独立性,尽可能降低依赖关系,降低业务间耦合度。此类不具备具体业务逻辑。

迪米特法则:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

  • Pharmacy.Stock.Ord.Imp
    • 例如对外提供接口判断订单是否存在。只需要引用Filter类即可。
Class Pharmacy.Stock.Ord.Imp Extends %RegisteredObject
{

// todo:对外提供接口

/// desc:  对外提供接口是否存在
ClassMethod IsOrdExist(ID As %String) [ CodeMode = expression ]
{
##class(Pharmacy.Stock.Ord.Filter).IsExist()
}

}

  • Pharmacy.Stock.Ord.Ref
    • 引用外部接口判断入库是否存在。
Class Pharmacy.Stock.Ord.Ref Extends %RegisteredObject
{

// todo:引用外部接口

/// desc:  引用外部接口,判断入库单是否存在
ClassMethod IsOrdExist(ID As %String) [ CodeMode = expression ]
{
##class(Pharmacy.Stock.Ins.Imp).IsInsExist()
}

}

Api

image

仅供前端直接访问的接口类,借用设计模式门面的思想。前端不允许访问除Api类之外的任何其他类方法,减少耦合度,针对接口编程。 Api可选择直接调用Biz方法,亦可覆写。对于前台需要的特殊格式的JsonXml,可以统一做转换。

Class Pharmacy.Stock.Ord.Api Extends Pharmacy.Stock.Ord.Biz
{

/// desc:查询明细
ClassMethod Query() [ CodeMode = expression ]
{
##super()
}

/// desc:根据保存单据
ClassMethod QueryById(params As %String) [ CodeMode = expression ]
{
##super(params)
}

}

注:这里应该当区分Api类与接口ImpRef的区别。Api是供前端调用的接口例如给webandroidios这些前端提供的数据接口。ImpRef为模块之间调用的数据接口。

Util

image

这里把Util类单独拿出来说,是因为防止重复造轮子,例如国密算法,每个组都可能用到,难道每个组都要去自己去实现吗?简单的还好,复杂的无疑增加了时间成本。

这里一定要明确Util类的概念,不包含任何业务逻辑方法,数据,过滤条件。与业务相关的,不能放到Util类里。Uti类更多是针对与编程方面,获取环境变量,公用算法等。

Util类是单独的包,面相所有产品,功能,模块的类,不通过接口来外部引用。这也说明了Util类一旦公布就只允许拓展,不允许修改。

传统的方式是,每个产品组各自为营,个搞个的,或者说压根就没有工具类,用到时在业务里随便复制粘贴个方法做为引用。所以就造成单个类,什么方法都有。

非常重要的一点是,工具类新增方法要定期公示,否则其他开发人员并不知道工具类有什么方法,或者建立索引文档提供查找。

总结

系统重构是大动作,持续是加比较长,而且会占用一定的资源,开发和测试。花费大量的沟通成本。

一旦决定重构,一开始就制定好各种规范,每个人都严格遵守。把解决的问题根据优先级、重要性。实施难度划分、

重构时可以遵循先易后难原则,这也是笔者的的一个重要体会。因为如果先攻克最难的问题,往往都耗时比较长,可能一两个月都没有什么进展和成果,会影响相关人员对项目的评价和看法,更会打击自信心。最后,刚开始的分析并不一定全面,所以一开始对最难的或最关键的是问题的判断可能会出错。

采取先易后难能够比较快速地看到成果,对后续项目的推进与提升士气有很大的好处。随着项目的进行,原来遗漏的点,或者分析和判断错误的点,会逐渐显示出来,到最后最难的问题也许迎刃而解。

以上是针对医疗领域与后端的一些业务开发框架的个人想法,现在回过头来看,感觉是理所当然,但实际上当时做分析和决策时远远没有这么简单。

以上是个人对基于IRIS后端的医疗框架理解,由于个人能力有限,欢迎大家提出意见,共同交流。

1
0 242
专家: 21
喜欢:
文章 王喆 👀 · 九月 21, 2023 12m read

前言

  生产环境下我们部署和使用IRiS引擎,往往采用其主备镜像模式,虽然此架构简单但是往往我们需要持续在电脑前点击或者操作1到2小时,如果中间有个环节出现了问题有时我们可能需要部署一天.

  接下来我分享的是IRIS自带的一个功能帮助我们部署---manifest-安装清单。他的主要使用方式是提前通过配置约定好我们期望的安装设置,在安装的过程中由IRIS程序直接执行脚本,简化IRIS集群的部署,减少运维人员的操作步骤,让我们有更多的精力放在实际项目和业务上。

1 简介

  %Installer 实用程序允许您定义描述和配置特定 InterSystems IRIS 配置的安装清单,而不是分步安装过程。为此,我们需要创建一个类,其中包含描述所需配置的 XData 块,使用包含通常在安装期间提供的信息(超级服务器端口、操作系统等)的变量。我们还可以在类中包含一个使用 XData 块生成代码以配置实例的方法。本文提供了安装清单的示例,您可以复制和粘贴这个示例尝试使用。

定义清单后,可以在安装期间、从终端会话或代码调用它。注意:清单必须在 %SYS 命名空间中运行。

2 Manifest的最终成品

此成品展示的是一个一键安装主、备、仲裁的机器命令,此方法的使用可以便捷快速的安装主备环境,其基本每一行都有注释其说明:

5
0 390
专家: 14
喜欢:
文章 Yuxiang Niu · 十一月 12, 2023 3m read

在日常Cache运维过程中可能会由于数据或者程序等原因造成锁的异常增长,导致数据库性能受到影响会出现程序报错或卡顿无法正常运行的问题。遇到此类问题需查看数据库当前锁列表情况,找到出现次数最多关键锁,根据关键锁对应的进程来判断处理。总结有以下三种方式查看关键锁。

  1. 可在portal[Locks]中查看;
  2. 可在terminal端的%sys下使用Do ^LOCKTAB命令下查看;
  3. 通过自定义程序查看。

查看方式

优点

缺点

第一种

易操作、方式简便

慢、锁数量太多无法显示

第二种

快、不受网页限制

易忘、需要输入准确命令

第三种

快、灵活、直接显示关键锁信息

需定位准确命名空间

下面给出自定义程序实例,程序逻辑为按命名空间循环所有锁信息,通过计数器方式记录所有锁当中出现次数最多的一个,输出其信息。入参为数据库中不同命名空间,输出结果为锁名称及锁的所有者,所有者一般为进程IDECP

2
0 202
专家: 13
喜欢:
文章 liu bo · 九月 21, 2023 4m read

前言

对于第三方接口进行交互的时候,往往需要大量的进行参数合法性校验。以前的方法就是对每个参数进行验证。如下截图: image

上图的会存在大量的if else if else..,如果字段很多,那导致一个方法存在大量的验证的代码,那我们考虑是否可以进行统一的验证参数的合法性。

思路

平时建立类的时候我们可以写参数MAXLEN=100,TRUNCATE=1 是否截取等,那找找这些参数的定义地方。如截图:

image 那我们想要定义自己的参数,该如何定义呢?根据面向对象设计原则之一:

里氏替换原则(Liskov Substitution Principle,LSP):子类型必须能够替换掉他们的基类型。即,在任何父类可以出现的地方,都可以用子类的实例来赋值给父类型的引用。当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有是一个 (is-a) 关系

那我们可以自定义数据类型,继承%Library.String,这样子类继续使用父类的参数,还可以自定义自己的参数。此处以字符串为例,其他的数据类型一样的原理。 自定义类型就为String.

实现

  1. 自定义数据类型

`

/// 自定义数据类型实现继承的String
Class Design.DataType.String Extends %Library.String
{

/// 是否为空 1 必填 0 可以为空
Parameter NOTBLANK = 0;

/// 代码值,写取global的表达式? $XECUTE 执行?
Parameter DICCODE;

/// 不为空的错误消息
Parameter MESSAGE;

/// 错误码值错误
Parameter CODEERRMESSAGE;

/// 类型 INT,STRING,FLOAT,NUMBER,DATE,DATETIME
Parameter TYPE = "STRING";

/// 是否时间类型
Parameter ISDATE = 0;

/// 时间格式:yyyy-MM-dd=>3  yyyyMMdd=8 dd/MM/yyyy=1	默认
Parameter DATEFORMAT = 3;

/// 条件取值验证
Parameter CONDITION;

/// 是否需要在当前时间之后,比如预约时间
Parameter ISAFTER;

/// 是否在当前直接之前 比如出生日期
Parameter ISBEFORE;

}

`

2.定义模型类 `

Class Design.DataType.Person Extends (%RegisteredObject, %XML.Adaptor)
{

/// 姓名
Property pname As String(MAXLEN = 100, MESSAGE = "人员姓名不能为空!", NOTBLANK = 1, TRUNCATE = 1);

/// 生日
Property birth As String(DATEFORMAT = 3, ISBEFORE = 1, ISDATE = 1, MESSAGE = "出生日期不能为空!", NOTBLANK = 1, TRUNCATE = 1);

/// 性别
Property sexCode As String(CODEERRMESSAGE = "性别代码错误!", DICCODE = "$O(^CT(""SEX"",0,""Code"",", MAXLEN = 100, MESSAGE = "性别代码不能为空!", NOTBLANK = 1);

/// ...此处省略  民族,国籍,学历等等
/// 工作描述
Property job As Job;

}

`

3.统一验证的方法代码 `

/// 校验对象的工具类
Class Design.DataType.ValidUtil Extends %RegisteredObject
{

/// 判断对象是否有效
/// TODO:嵌套对象可以自己研究研究
ClassMethod IsValid(obj As %ObjectHandle, Output errmsg As %String) As %Status
{
	s ClsName=obj.%ClassName(1)
	s validFlag=$$$OK
	s Name=""
	f{
		s Name=$o(^oddDEF(ClsName,"a",Name)) 
		q:Name="" 
	    s Val=$Property(obj,Name)
	    w Name_"="_Val,!
	    ;获取属性对应的参数
	    ;是否为空
	    s NOTBLANK=$g(^oddDEF(ClsName,"a",Name,"P","NOTBLANK"))
	    i (NOTBLANK=1)&&(Val=""){
		  s errmsg= $g(^oddDEF(ClsName,"a",Name,"P","MESSAGE"))  
		  s validFlag=0
		  q 
		}
		;是否码值校验
		s DICCODE=$g(^oddDEF(ClsName,"a",Name,"P","DICCODE"))
		i DICCODE'=""{
		   SET cmd="(out){ s out="_DICCODE_""""""_$$ALPHAUP^SSUTIL4(Val)_""",""""))"_" q 1}"
		   SET rtn=$XECUTE(cmd,.rowId)
		   i rowId="" {
			  s errmsg=$g(^oddDEF(ClsName,"a",Name,"P","CODEERRMESSAGE"))
			  s validFlag=0
			  q 
		   } 
		}
		;是否时间格式校验
	    s ISDATE=$g(^oddDEF(ClsName,"a",Name,"P","ISDATE"))
	    if ISDATE=1 {
		   s DATEFORMAT=$g(^oddDEF(ClsName,"a",Name,"P","DATEFORMAT"))
		   s result=$zdh(Val,DATEFORMAT,"","","","","","-1","时间格式错误","")
           if result="时间格式错误"{
	          s errmsg="字段"_Name_"["_Val_"]"_"时间格式错误"
	          s validFlag=0
			  q  
	       }
	       //时间与当前时间的验证
	       s ISBEFORE=$g(^oddDEF(ClsName,"a",Name,"P","ISBEFORE"))
	       if ISBEFORE=1{
		      if result>+$h {
			      s errmsg="字段"_Name_"["_Val_"]"_"不能超过当前日期!"
			      s validFlag=0
				  q  
		      }
		   }
		   s ISAFTER=$g(^oddDEF(ClsName,"a",Name,"P","ISAFTER"))
	       if ISAFTER=1{
		      if result<+$h {
			      s errmsg="字段"_Name_"["_Val_"]"_"不能小于当前日期!"
			      s validFlag=0
				  q  
		      }
		   }
		}
	}
    q validFlag
}

}

`

测试

  1. 测试不为空 image

  2. 测试code错误 image

3.测试时间格式错误 image

4.测试时间的值先后 image

[^1]

**参数还需要进行大量的验证,此处只是示例,可能存在错误,欢迎批评纠正**

[^1]:

1
1 206
专家: 11
喜欢:
文章 Yongfeng Hou · 十一月 23, 2023 3m read

        IRISHealth以其完备且系统化的安全特性在医疗行业的数据库中独树一帜,这些特性包括安全认证、安全授权、安全审计、数据加密以及安全配置。其中数据传输无疑是其中最重要的一环。为此,IRISHealth采用了SSL/TLS技术来对传输的数据进行加密,有效保障了从IRIS数据平台的超级服务数据传输、Telnet服务数据传输、java/.net/Studio客户端的访问数据传输、MIRROR与DB的数据传输,到DBServer和ECPApp之间的数据传输的安全性。


        本文是在两个IRISHealth2021实例之间进行ECP服务通信的示例,一个作为DBServer,一个作为ECPApp,两个实例之间通过使用SSL/TLS的ECP协议进行TCP的加密传输通信。

1.IRIS的DB和ECP环境:

DBServer 

ECPApp

10.1.30.231  10.1.30.232

 

2. CA证书的环境:

5
3 380
专家: 6
喜欢:
文章 water huang · 十月 6, 2023 4m read

一般情况下,我们根据iris的portal向导创建数据库,然后创建命名空间。这个过程比较花时间,如果是已经存在的数据库,还需要再装载。翻阅portal调用的方法后,我整合了这几个方法。把这几个方法拷贝到任意已经存在的命名空间,通过执行CNNS(路径,命名空间),就可以快速创建好命名空间。方法的大概过程是,进入到%sys命名空间,然后依次创建数据库,创建命名空间,创建web应用。创建完成后,回到当前命名空间。

0
0 192
专家: 6
喜欢:
文章 Yinhang Hao · 十一月 20, 2023 2m read

前言

在日常工作中经常会遇到大量的接口开发需求,对于没有IRIS开发经验的同事来说很不友好,需要求助于公司开发人员来做接口开发,对项目联调进度多少会有些影响,本文站在没有IRIS开发经验的工作人员角度来阐述一下如何利用xslt转换文件自动生成接口联调所需要的Message模型。

基本思路是首先定义一套通用的数据模型,用来接收定义消息所需要的基础属性,包括类名,请求&响应(对应继承Ens.Request&Ens.Response),节点名称、节点长度、是否必填、默认值、字段约束等等。

1
0 165
专家: 5
喜欢:
文章 liu bo · 九月 19, 2023 4m read

前言 {#1}

ensemble里边实现分页比较麻烦,毕竟对于sql的书写比较麻烦,单表的查询相对简单,对于多表的关联查询单纯的sql不好查询,我们使用sql进行先查询出主表满足条件的rowId,在根据根据满足条件的rowid进行遍历取值。

思路

我们先取对比一下其他数据库实现的原理。

  1. Mysql的实现原理 总数:SELECT COUNT(*) AS total FROM person WHERE (name LIKE ?) 分页:SELECT id,name,age,email FROM person WHERE (name LIKE ?) LIMIT ?,?

  2. ORACLE的实现原理 rownum 总数:SELECT COUNT() AS total FROM person WHERE (name LIKE ?) 分页:SELECT * FROM ( SELECT TMP., ROWNUM ROW_ID FROM ( SELECT id,name,age,email FROM person WHERE (name LIKE ?) ) TMP WHERE ROWNUM <=?) WHERE ROW_ID > ?

  3. 由于cache没有limit关键字,看看有没有和oracle里边rownum一样的原理。Cache的实现原理和oracle类似 %VID 只查询主键id,在遍历取值 总数:select count() FROM Design_Page.Person WHERE birth<'1988-12-1' 分页:SELECT * FROM ( SELECT %VID ROWNUM ,TMP. FROM ( SELECT * FROM Design_Page.Person WHERE birth<'1988-12-1' ) TMP WHERE %VID <=15) WHERE ROWNUM > 5

代码构建

  1. 构建查询的抽象的AbstractQueryWrapper包装类 `

    Class Design.Page.V1.AbstractQueryWrapper Extends %RegisteredObject {

        /// 构建sql的运算符号
        Parameter AND = "AND";
    
        Parameter OR = "OR";
    
        Parameter NOT = "NOT";
    
        Parameter IN = "IN";
    
        Parameter NOTIN = "NOT IN";
    
        Parameter LIKE = "LIKE";
    
        Parameter NOTLIKE = "NOT LIKE";
    
        Parameter EQ = "=";
    
        Parameter NE = "!=";
    
        Parameter GT = ">";
    
        Parameter GE = ">=";
    
        Parameter LT = "<";
    
        Parameter LE = "<=";
    
        Parameter ISNULL = "IS NULL";
    
        Parameter ISNOTNULL = "IS NOT NULL";
    
        Parameter GROUPBY = "GROUP BY";
    
        Parameter HAVING = "HAVING";
    
        Parameter ORDERBY = "ORDER BY";
    
        Parameter EXISTS = "EXISTS";
    
        Parameter NOTEXISTS = "NOT EXISTS";
    
        Parameter BETWEEN = "BETWEEN";
    
        Parameter NOTBETWEEN = "NOT BETWEEN";
    
        Parameter ASC = "ASC";
    
        Parameter DESC = "DESC";
    
        /// 抽象类
        Method addCondition(coloumParams As %String) [ Abstract ]
        {
        }
    
        /// 添加字段之间的条件连接
        Method addConditionOperate(operate As %String) [ Abstract ]
        {
        }
    
        /// 等于的条件
        Method eq(column As %String, val As %String)
        {
           d ..addCondition(" "_column_..#EQ _"'"_val_"' ")
           q $this
        }
    
        /// 不等于
        Method ne(column As %String, val As %String)
        {
           d ..addCondition(" "_ column_..#NE _"'"_val_"' ")
           q $this
        }
    
    /// 大于的条件
    Method gt(column As %String, val As %String)
    {
       d ..addCondition( " "_column_..#GT _"'"_val_"' ")
       q $this
    }
    
    /// 大于等于的条件
    Method ge(column As %String, val As %String)
    {
       d ..addCondition( " "_column_..#GE _"'"_val_"' ")
       q $this
    }
    
    /// 小于的条件
    Method lt(column As %String, val As %String)
    {
       d ..addCondition(" "_column_..#LT _"'"_val_"' ")
       q $this
    }
    
    /// 小于等于条件
    Method le(column As %String, val As %String)
    {
       d ..addCondition( " "_column_..#LE _"'"_val_"' ")
       q $this
    }
    
    /// like 模糊匹配
    Method like(column As %String, val As %String)
    {
       d ..addCondition( " "_column_" "_..#LIKE _" '%"_val_"%' ")
       q $this
    }
    
    /// not like 模糊匹配
    Method notLike(column As %String, val As %String)
    {
       d ..addCondition( " "_column_" "_..#NOTLIKE _" '%"_val_"%' ")
       q $this
    }
    
    /// 左匹配 模糊匹配
    Method likeLeft(column As %String, val As %String)
    {
       d ..addCondition( " "_column_" "_..#LIKE _" '"_val_"%' ")
       q $this
    }
    
    /// 右匹配
    Method likeRight(column As %String, val As %String)
    {
       d ..addCondition( " "_column_" "_..#NOTLIKE _" '%"_val_"' ")
       q $this
    }
    
    /// between 拼接
    Method between(column As %String, startVal As %String, endVal As %String)
    {
       d ..addCondition( " "_column_" "_..#BETWEEN _" '"_startVal_"' "_..#AND_"'"_endVal_"' ")
       q $this
    }
    
    /// notBetween 拼接
    Method notBetween(column As %String, startVal As %String, endVal As %String)
    {
       d ..addCondition( " "_column_" "_..#NOTBETWEEN _" '"_startVal_"' "_..#AND_"'"_endVal_"' ")
       q $this
    }
    
    /// 字段值为空
    Method isNull(column As %String)
    {
       d ..addCondition( " "_column_" "_..#ISNULL _" ")
       q $this
    }
    
    /// 非空
    Method isNotNull(column As %String)
    {
       d ..addCondition( " "_column_" "_..#ISNOTNULL_" ")
       q $this
    }
    
    /// in 
    Method in(column As %String, valueList As %String, Separator As %String = "^")
    {
       d ..addCondition( " "_column_" "_..#IN_"("_..ConcatListParams(valueList,Separator)_")")
       q $this
    }
    
    /// not in 
    Method notIn(column As %String, valueList As %String, Separator As %String = "^")
    {
       d ..addCondition( " "_column_" "_..#NOTIN_"("_..ConcatListParams(valueList,Separator)_")")
       q $this
    }
    
    /// in list的参数拼接
    Method ConcatListParams(valList As %String, Separator As %String)
    {
    	s paramsLen=$l(valList,Separator)
    	q:paramsLen=1 "'"_valList_"'"
    	s paramsList =$lb("")
    	for i=1:1:paramsLen{
    	  if (i=1)  s $LIST(paramsList,1)=$p(valList,Separator,i)
    	  else  s $LIST(paramsList,*+1)=$p(valList,Separator,i)
    	}
    	q "'"_$LISTTOSTRING(paramsList,"','")_"'"
    }
    
    }
    

`

  1. 构建查询的包装类queryWrapper `

    /// 包装查询的列和查询条件以及运算符 Class Design.Page.V1.QueryWrapper Extends AbstractQueryWrapper {

    /// sql查询的列
    Property SelectColoums As %String;
    
    /// 查询的表对应的schema
    Property querySchema As %String [ InitialExpression = "SQLUser" ];
    
    /// 查询的表
    Property queryTable As %String;
    
    /// 查询条件
    Property queryCondition As %String;
    
    /// 字段,值,关系运算符,逻辑运算符构建查询条件
    Method addCondition(coloumParams As %String)
    {
    	s ..queryCondition=..queryCondition_" "_coloumParams
    }
    
    /// and 连接字段
    Method and()
    {
       s ..queryCondition=..queryCondition_" "_..#AND
       q $this
    }
    
    Method or()
    {
       s ..queryCondition="("_..queryCondition_") "_..#OR
       q $this
    }
    
    }
    

3. 构建查询总数和过滤条件的sqlBuilder

/// 构建查询的sql语句
Class Design.Page.V1.SqlBuilder Extends %RegisteredObject
{

Property queryWrapper As QueryWrapper;

Method %OnNew(queryWrapper As QueryWrapper) As %Status [ Private, ServerOnly = 1 ]
{
	s ..queryWrapper=queryWrapper
	Quit $$$OK
}

/// 构建查询的总数据的sql
Method bulidCountSql()
{
	q "select count(*) totalcount from "_..queryWrapper.querySchema_"."_..queryWrapper.queryTable_" where"_..queryWrapper.queryCondition
}

/// 构建查询执行查询业务的sql
Method bulidBusiSql()
{
	q "select "_..queryWrapper.SelectColoums_" from "_..queryWrapper.querySchema_"."_..queryWrapper.queryTable_" where"_..queryWrapper.queryCondition
}

Method bulidPageSql(stOffset As %Integer, endOffset As %Integer)
{
	s businessSql=..bulidBusiSql()
	q "SELECT * FROM ( SELECT  %VID ROWNUM ,TMP.* FROM ( "_businessSql_") TMP WHERE %VID <= "_endOffset_" ) WHERE ROWNUM > "_stOffset
}

}

4. 构建返回数据列表的IPage

/// 分页插件
Class Design.Page.V1.IPage Extends %RegisteredObject
{

/// 数据列表
Property Data As list Of %RegisteredObject;

/// 查询列表总记录数 0
Property total As %Integer [ InitialExpression = 0 ];

/// 总页数
Property pages As %Integer [ InitialExpression = 0 ];

/// 每页显示条数,默认 10
Property pageSize As %Integer [ InitialExpression = 10 ];

/// 当前页
Property currentPage As %Integer [ InitialExpression = 1 ];

/// 当前计数器
Property currentCount As %Integer [ InitialExpression = 0 ];

/// 单页分页条数限制
Property maxLimit As %Integer;

/// 分页的最后一次循环的ID
Property currentId As %String;

/// /插入数据
Method InternalInsert(obj As %ObjectHandle)
{
   q ..Data.Insert(obj)
}

/// 执行往list里边插入对象的操作
Method doInsert(obj As %ObjectHandle) As %Status
{
    s currentPage=..currentPage
	s pageSize=..pageSize
	s currentCount=..currentCount+1
	s ..currentCount=currentCount
	d:(currentPage=1) ..InternalInsert(obj)
    d:((currentCount>((currentPage-1)*pageSize))&&(pageSize>0)&&(currentPage>1)) ..InternalInsert(obj)
    ;实际的页数大于等于分页的数 退出循环
    q ..Data.Count()>=pageSize
}

/// 根据计算起始数和限制查询的条数
Method getOffset(Output stOffset, Output endOffset)
{
	 ;分页数
	 i ..total # ..pageSize=0  d
	 .s ..pages= ..total/..pageSize
	 e  s ..pages=$System.SQL.FLOOR(..total/..pageSize) +1
	 ;当前页数
     s currentPage = ..currentPage
     i currentPage=1{
	     s stOffset=0
	     s endOffset=..pageSize
     }else{
	     s stOffset=(currentPage-1)*..pageSize
	     s endOffset=currentPage*..pageSize
     }
     q $$$OK
}

/// 获取查询的结果的ID
Method selectPage(queryWrapper As QueryWrapper, Output ok) As %ArrayOfDataTypes
{
	s ret = ##Class(%ArrayOfDataTypes).%New()
	//拼接sql执行查询总数
	s ok=$$$OK
	s sqlBuilder=##class(Design.Page.V1.SqlBuilder).%New(queryWrapper)
	s countTotalSql=sqlBuilder.bulidCountSql()
	d ..exeCountTotalSql(countTotalSql)
	q:..total=0 ret
	///计算分页
	d ..getOffset(.stOffSet,.edOffSet)
	///获取分页执行sql
	s pageSql=sqlBuilder.bulidPageSql(stOffSet,edOffSet)
    ///返回结果集的ID
	q ..exePageSql(pageSql)
}

/// 执行查询分页sql
Method exePageSql(sql) As %ArrayOfDataTypes
{
	s ret = ##Class(%ArrayOfDataTypes).%New()
    s rset = ##class(%ResultSet).%New()
	d rset.Prepare(sql)
	d rset.Execute()
	i rset.QueryIsValid() d
	.While (rset.Next()) {
	.d ret.SetAt(rset.GetData(1),rset.GetData(2))
	.}
	q ret
}

/// 执行查询总数的sql
Method exeCountTotalSql(sql) As %Status
{
    s rset = ##class(%ResultSet).%New()
	d rset.Prepare(sql)
	s sc= rset.Execute()
	i rset.QueryIsValid() d
	.While (rset.Next()) {
	. s ..total= rset.GetData(1)
	.}
	q $$$OK
}

}

`

测试

  1. 自定义的objlist需要继承IPage `

           ///定义返回的对象列表
            Class Design.Page.ObjList Extends (Design.Page.V1.IPage, %XML.Adaptor)
               {
    
                        /// 数据列表
                        Property Data As list Of Object;
    
               }
                 ///单个对象
                Class Design.Page.Object Extends (%RegisteredObject, %XML.Adaptor)
                {
    
                Property PatientName As %String;
    
                Property PatientNo As %String;
    
                }
    

`

2.测试代码

`

/// 分页查询
ClassMethod selectPage()
{
	s $zt="Err"
	//当前页数
	s currentPage=1
	//每页的大小
	s pageSize=10
	s objlist=##class(Design.Page.ObjList).%New()
	s objlist.currentPage=currentPage
	s objlist.pageSize=pageSize
	//构建查询的条件
	s queryWrapper =##class(Design.Page.QueryWrapper).%New()
	s queryWrapper.SelectColoums="ID"
	s queryWrapper.querySchema="Design_Page"
	s queryWrapper.queryTable="Person"
	d queryWrapper.lt("birth",$zdh("2023-12-1",3)).and().like("name","in")
	;执行查询查询获取Id
	s rset=objlist.selectPage(queryWrapper,.ok)
	q:ok'=1 "调用出现异常"
	q:objlist.total=0 "未查询到数据!"
	q:rset.Count()=0 "未查询到数据!"
	s RowId=""
	while(rset.GetNext( .RowId)){
		continue:'$d(^Design.Page.PersonD(RowId))
		s obj=##class(Design.Page.Object).%New()
		s obj.PatientName=$lg(^Design.Page.PersonD(RowId),2)		;患者姓名
	    s obj.PatientNo=$lg(^Design.Page.PersonD(RowId),3)		;病人ID号
	    d objlist.Data.Insert(obj)
	}
	w objlist.Data.Count(),!
	d objlist.XMLExportToString(.xml)
	w xml,!
	q
Err
   w $ze,!
   q $$$OK
}

`

3.查询结果 `

<ObjList>
	<total>23</total>
	<pages>3</pages>
	<pageSize>10</pageSize>
	<currentPage>1</currentPage>
	<currentCount>0</currentCount>
	<Data>
		<Object>
			<PatientName>Ingrahm,Michelle X.</PatientName>
			<PatientNo>436244981</PatientNo>
		</Object>
		<Object>
			<PatientName>Koivu,Clint W.</PatientName>
			<PatientNo>473036353</PatientNo>
		</Object>
		<Object>
			<PatientName>Avery,Josephine F.</PatientName>
			<PatientNo>815934238</PatientNo>
		</Object>
		<Object>
			<PatientName>Thompson,Clint M.</PatientName>
			<PatientNo>970071592</PatientNo>
		</Object>
		<Object>
			<PatientName>Ingersol,Diane S.</PatientName>
			<PatientNo>949798228</PatientNo>
		</Object>
		<Object>
			<PatientName>Quince,Sally E.</PatientName>
			<PatientNo>643134733</PatientNo>
		</Object>
		<Object>
			<PatientName>Novello,Clint Y.</PatientName>
			<PatientNo>612491568</PatientNo>
		</Object>
		<Object>
			<PatientName>Ingrahm,Buzz O.</PatientName>
			<PatientNo>72704061</PatientNo>
		</Object>
		<Object>
			<PatientName>Ihringer,Chris M.</PatientName>
			<PatientNo>112730429</PatientNo>
		</Object>
		<Object>
			<PatientName>Anderson,Vincent V.</PatientName>
			<PatientNo>507161056</PatientNo>
		</Object>
	</Data>
</ObjList>

`

2
0 355
专家: 5
喜欢:
文章 Meng Cao · 十一月 23, 2023 2m read
  • 前言 

        随着网络安全日益被重视,特别是等级保护制度的大环境下,SSL加密传输越来越被重视,本文介绍如何使用支持SSL的ODBC连接IRIS数据库。

        数据库版本:IRISHealth-2023.1

  • 1. 服务器端配置

       1)新建SSL服务器配置。

    

         2)开启超级端口的SSL支持,这里我们选择启用即可,如所有超级端口的连接都支持SSL可选要求。

       

  • 2.Windows客户端配置

       1)创建 SSLDefs.ini 配置文件,并编辑内容:

       [TLS to an InterSystems IRIS instance]
       Address=127.0.0.1
       Port=51773
       SSLConfig=DHCC

5
3 572
专家: 4
喜欢:
文章 water huang · 十月 6, 2023 3m read

iris 是数据平台,更是一种数据库。对于熟悉SQL语句的人来说,会认为“既然是数据库,数据应该就能使用sql语句来查询”。这是对的,但是因为有global这个概念,保存的数据可能在global里面,而没有对应的表,也可能保存在类的参数定义里面。这些数据,不能使用sql直接查询。要查询iris数据库的数据,通常有几种方式:1.直接查询表的数据。2.查询视图。3.调用存储过程(call 命令)。其中要查询“只存于global里面或者类参数里定义的数据”,只有使用存储过程。但是存储过程有个问题,就是程序如果迁移到低版本的cache数据库后,数据类型的定义会有问题,且不再支持使用select的方式,只能使用call。这对于第三方熟悉sql的人员来说很不友好。因此结合global和表的关系,介绍一种我称为“进程表”的表。进程表,指数据只存于该进程中,global的样式为"^||global名“。通常按照默认存储新加一个持久类(对应会生成一个表),然后手动的把global改成进程global,也就是加上”||“。然后写个方法,把需要查询出来的数据写入进程global。这样就能查询出来 了。调用形式为 SELECT * FROM People WHERE People_GLB()=1。

示例如下:

2
0 199