实体 Facade
基本的 CRUD 操作
通过 EntityValue 接口可对一个实体记录机进行基础CRUD操作。主要有两种方式获得一个 EntityValue 对象:
- 生成一个值(使用 ec.entity.makeValue(entityName))
- 查找一个值(详情如下)
一旦你有了一个 EntityValue 对象,你可以调用 create()、update()或delete()方法来执行想要的操作。也有一个createOrUpdate() 方法将在不存在时创建、存在时更新。
注意所有这些方法,像 EntityValue 接口上的许多方法,为方便返回一个对自身的引用,便于你能把操作链起来。例如:
ec.entity.makeValue("Example").setAll(fields).setSequencedIdPrimary().create()
这个例子比较有趣,只在很少的情况下,你会直接使用EntityFacade API(ec.entity)创建一条记录。一般你应当通过服务来执行CRUD操作,通过ServiceFacade有对所有实体的自动CRUD的服务可用。这些服务没有定义,隐式的存在,只由实体定义驱动。
我们后续将在逻辑层更详细地讨论 ServiceFacade,但是这有一个使用隐含的自动实体服务的大概的例子:
ec.service.sync().name("create#Example").parameters(fields).call()
Moqui框架API的大部分方法返回一个对自身的引用,以便像这样将调用链起来。两者之间的主要区别在于一个是通过ServiceFacade,另一个不是。通过ServiceFacade有一些好处(例如事务管理、流控制、安全等等),但是两个调用间很多事情是一样的,包括在继续执行底层操作前的对传入字段的自动清理、类型转换。
使用隐含的自动实体服务,你不必明确为序列主键ID设值,因为如果有一个单主键时它会自动判断,如果没有在服务的入参中,它会生成一个。
不管你怎么执行这些操作,只有修改了的或传入的实体字段会被修改。EntityValue对象将记录哪个字段已被修改,并只在操作在数据库中完成了才创建或更新跟踪记录。可以使用isModified()方法来判断一个EntityValue对象是否被修改,并且可以用refresh()方法来恢复它在数据库中的状态(恢复所有字段,而不仅仅是被修改了的)。
如果你像查找EntityValue中的当前字段值和数据库中对应的列值之间的区别,使用checkAgainstDatabase(List messages)方法。此方法被用在保存(相对于加载)entity-facade-xml文件时校验、断言时,如果你想编写Java或Groovy代码检查数据状态也可以使用它。
查找实体记录
查找实体记录是使用 EntityFind 接口完成的。与其使用许多不同方法带上不同的可选参数,不如使用 EntityFind 接口,可以调用你关心的方面的方法,并忽略其余的。你可以像这样获得一个find对象:
ec.getEntity().find("moqui.example.Example")
EntityFind接口上的大部分方法返回一个到对象的引用,这样你可以将方法调用链起来,而不是分成多条语句。例如Example实体上的按主键查找会像这样:
EntityValue example = ec.entity.find("moqui.example.Example").condition("exampleId", exampleId).useCache(true).one()
EntityFind接口上的方法有这些:
- 条件(where和having都有)
- condition(String fieldName, Object value): 简单条件,指定名称的字段和值相等
- condition(String fieldName, EntityCondition.ComparisonOperator operator, Object value): 使用运算符比较指定名称的字段和值。操作符可以是 EQUALS, NOT_EQUAL, LESS_THAN, GREATER_THAN, LESS_THAN_EQUAL_TO, GREATER_THAN_EQUAL_TO, IN, NOT_IN, BETWEEN, LIKE, 或 NOT_LIKE。
- conditionToField(String fieldName, EntityCondition.ComparisonOperator operator, String toFieldName): 使用运算符比较一个字段和另一个字段。
- condition(Map
fields): 由Map中的每个条目来约束,条目的键匹配实体上的字段名。如果一个字段的名字和任一Map的键名一样,将会用Map的键值替换字段值。按这种方式设置的字段在执行查询前将会结合其他(可用的)条件。如果需要将会按需执行从字符串到字段类型的转换,并且只获取匹配实体字段的键值。也就是说,它和 EntityValue.setFields(fields, true, null, null) 做了同样的事情。 - condition(EntityCondition condition): 添加一个通过 EntityConditionFactory 创建的条件
- conditionDate(String fromFieldName, String thruFieldName, Timestamp compareStamp): 为标准有效日期查询模式添加条件,包括from字段为null或早于或等于compareStamp,thru字段为null或晚于或等于compareStamp。
- havingCondition(EntityCondition condition): 添加一个通过EntityConditionFactory创建的条件到having条件。Having是标准SQL的概念,被用于grouping和functions后的条件。
- searchFormInputs(String inputFieldsMapName, String defaultOrderBy, boolean alwaysPaginate): 为inputFieldsMapName Map种超导的字段添加条件。字段和有支持的后缀的特定字段和XML表单种的*-find字段一样。这意味着你可以使用这个来处理由XML表单生成的各种输入数据。后缀包括像给运算符的*_op和为了忽略大小写的*_ic。如果inputFieldsMapName是空的,将在web facade可用时查找ec.web.parameters映射表,否则查找当前上下文(ec.context)。如果没有orderByField参数(查找XML表单的一个标准参数)将会使用defaultOrderB。如果alwaysPaginate为true,即使没有pageIndex参数,也会设置分页的offset/limit。
- 用来选择返回那些字段: selectField(String fieldToSelect) 和/或 **selectField(Collection
fieldsToSelect) - 用来对结果排序的字段
- orderBy(String orderByFieldName): 用来排序的被查找实体的字段。可选地加上"ASC"在后面或"+"在前面表示升序,或者"DESC"在后面“-”在前面表示降序。如果已指定其他的order by字段,那么这个将被加在字段列表的后面。字符串可以是一个逗号分隔的字段名列表。只有实体上实际存在的字段将被加到order by列表。
- orderBy(List
orderByFieldNames): 每个列表条目被传递到orderBy(String orderByFieldName) 方法。
- 是否缓存结果: useCache(Boolean useCache),默认为实体定义上的值
- 传到数据源来限制结果的 offset 和 limit
- offset(Integer offset): offset,返回的起始行。默认(null)表示从实际的第一行开始。只能应用于 list() 和 iterator() 查找。
- offset(int pageIndex, int pageSize): 以分页的索引号和大小的方式指定偏移量。实际的偏移量为 pageIndex*pageSize
- limit(Integer limit): limit,返回的最大行数。默认(null)意思是所有行。只能应用于 list() 和 iterator() 查找。
- 数据库选项
- distinct: distinct(boolean distinct)
- update: forUpdate(boolean forUpdate)
- JDBC选项
- resultSetType(int resultSetType): 指定将如何遍历ResultSet。可用的值有ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE (默认的) or ResultSet.TYPE_SCROLL_SENSITIVE。更多信息请查看 java.sql.ResultSet JavaDoc。如果你想它快,确保使用的是默认的 ResultSet.TYPE_SCROLL_INSENSITIVE。
- resultSetConcurrency(int resultSetConcurrency)。指定ResultSet能否被更改。可能的值有ResultSet.CONCUR_READ_ONLY (默认) 或 ResultSet.CONCUR_UPDATABLE。用EntityFacade时应当总是使用ResultSet.CONCUR_READ_ONLY,因为更新通常是作为分开的操作完成的。
- fetchSize(Integer fetchSize):此查询的 JDBC fetch大小。默认(null)将会退回到数据源设置。这不是OFFSET/FETCH SQL语句中的fetch(这时使用offset/limit方法),而是JDBC每次从数据库获取数据时返回多少行。只能应用于 list() 和 iterator() 查找。
- maxRows(Integer maxRows): 此查询的JDBC最大行数。默认(null)将会回退到数据源设置。这是在任意给定时刻释放ResultSet之前将保留在内存的最大行数。如果释放后被请求到,将会从数据库再次获取它们。只能应用于 list() 和 iterator() 查找。
条件有多种选项,一些在EntityFind接口自身上,通过EntityConditionFactory接口有一个更广的集合。使用 ec.entity.getConditionFactory() 方法获取此接口的一个实例,就像:
EntityConditionFactory ecf = ec.entity.getConditionFactory();
ef.condition(ecf.makeCondition(...));
对于遵循标准Moqui模式(被用在XML表单查找字段中,也能被用在模板、JSON、或XML参数体中)的查找表单,使用EntityFind.searchFormInputs() 方法。
一旦指定了所有这些选项,你可以执行任意这些实际的操作来获取结果或做变更:
- 获得单个 EntityValue: one() 方法
- 获取一个有多个值对象的 EntityValueList: list() 方法
- 获取一个 EntityListIterator 来以小批量的方式处理大数据量的结果集: iterator() 方法
- 过去匹配结果的数量: count() 方法
- 用指定的字段更新所有匹配的记录: updateAll() 方法
- 删除所有匹配的记录: delete() 方法
使用视图实体进行灵活地查找
你可能注意到 EntityFind 接口在单个实体上操作。要执行一个跨多个实体联结在一起的查询,并用一个单实体名字表示,你可以用一个和正常实体定义一起的XML定义创建一个静态视图实体。
视图实体也能被定义在数据库记录中(在 DbViewEntity 和相关实体中)或者用 EntityDynamicView 接口编码构建的动态视图实体(用 EntityFind.makeEntityDynamicView()方法获取一个实例)。
静态视图实体
一个视图实体有一个或多个成员实体关联在一起组成,有键映射,一组别名来自成员实体的字段,还可能有一些函数与它们关联。视图实体也有条件与其关联,以封装数据上的某类约束,以被包含在视图中。
这里是example组件中 ExampleViewEntities.xml 文件中的视图实体XML片段:
<view-entity entity-name="ExampleFeatureApplAndEnum" package="moqui.example">
<member-entity entity-alias="EXFTAP" entity-name="ExampleFeatureAppl"/>
<member-entity entity-alias="ENUM" entity-name="moqui.basic.Enumeration" join-from-alias="EXFTAP">
<key-map field-name="exampleFeatureApplEnumId"/>
</member-entity>
<alias-all entity-alias="EXFTAP"/>
<alias-all entity-alias="ENUM">
<exclude field="sequenceNum"/>
</alias-all>
</view-entity>
就像一个实体,一个视图实体有一个名字,存在于一个包中,分别使用view-entity元素上的entity-name和package-name属性。
每个成员实体由一个 member-entity 元素表示,由一个 entity-alias 属性中的别名唯一标识。部分原因在于同一个实体可能在一个视图实体中出现多次,每个使用不同的别名。
注意第二个 member-entity 元素有一个 join-from-alias 属性来指定它是联结到第一个成员实体。只有第一个成员实体没有 join-from-alias 属性。如果你想要当前成员实体在联结中是可选的(SQL中的左外联结),设置 join-optional 属性为 true。
要描述两个实体是如何互相关联,在member-entity元素下使用一个或多个key-map元素。key-map元素有两个属性:field-name和related。注意当与当前成员实体上的主键字段匹配时,related属性是可选的。
如上面的例子,可用 alias-all 元素或各自使用alias元素为字段在集合中设置别名。如果你想在字段上使用函数,则用 alias 元素单独给它们起个别名。注意SQL数据,如果任一起了别名的字段有一个函数,那么所有其他字段是没有函数的,查询中被选的字段将会被加到group by语句以避免无效的SQL。
todo: 这个的例子
视图实体对查找的自动最小化
用 EntityFacade的 EntityFind 做一个查询,你可以指定select的字段,并且只有那些字段会被select返回。对于视图实体,多做这么一点,却能带来性能上的大幅提升。
静态视图的一个共同的问题是你想关联一堆成员实体来为搜索页面和类似的灵活的查询提供许多的选项,当你这么做时,数据库中为此查询生成的临时表会变得非常大。通常的用法是只选择某些字段,只在有限的字段集上有条件和排序,你可能会联结进来一些实际没有用到的表。效果上你让数据库做了比你真正实际需要的数据要多得多的工作。
解决这个问题的方法之一是构建一个 EntityDynamicView ,只联结用于特定查询选项所需的实体。这样可行,但是很累赘。
简单的实现方法是利用 EntityFind 中为每个特定的查询自动最小化字段和关联进来的实体的特性。在一个视图实体上,只用指定要选择的字段、条件和排序字段。EntityFacade将自动翻找视图实体定义,并只别名化用于select、条件、排序中的字段,只关联入有实际使用的字段的实体(或那些连接一个成员实体到另一个以完成联结所需的实体)。
一个好例子是定义在 Mantle业务构件中的 PartyViewEntities.xml 中的 FindPartyView 视图实体。这个视图实体有可观的13个成员实体。没有自动最小化,其上的每个查询将联结13张表。要处理上百万条客户记录或其他这么大的人员数据,每个查询要花几分钟时间。只查询一些字段并只联结少量的成员实体并且最少的字段时,查询需时下降到亚秒级。
实际的查找由 mantle.party.PartyServices.find#Party 服务完成。此服务的实现时一个简单的45行的Groovy脚本(findParty.groovy)。并且脚本的绝大部分只是根据参数是否由指定来添加条件到查询。用 EntityDynamicView 实现做同样的事情,需要数百行更复杂的脚本,编写和维护都更复杂。
数据库定义的视图实体
除了在XML中定义视图实体外,你也可以用 DbViewEntity 和相关的实体在数据库中定义它们。这在用户为构建页面临时(像在tools组件中的EditDbView.xml页面,菜单 Tool => Data View)定义一个视图时特别有用,然后用一个基于用户定义的视图(像 ViewDbView.xml页面)来搜索、查看和导出数据。
没有定义数据库视图实体时那么多选项,但是主要特性还在,并且应用了同样的模式。一个视图实体由名字(dbViewEntityName)、包(packageName)和是否缓存结果。它也有成员实体(DbViewEntityMember)、指定成员如何联结在一起的键映射(DbViewEntityKeyMap)和字段别名(DbViewEntityAlias)。这有个example组件中的例子:
<moqui.entity.view.DbViewEntity dbViewEntityName="StatusItemAndTypeDb" packageName="moqui.basic" cache="Y">
<moqui.entity.view.DbViewEntityMember entityAlias="SI" entityName="moqui.basic.StatusItem"/>
<moqui.entity.view.DbViewEntityMember entityAlias="ST" entityName="moqui.basic.StatusType" joinFromAlias="SI"/>
<moqui.entity.view.DbViewEntityKeyMap joinFromAlias="SI" entityAlias="ST" fieldName="statusTypeId"/>
<moqui.entity.view.DbViewEntityAlias entityAlias="SI" fieldAlias="statusId"/>
<moqui.entity.view.DbViewEntityAlias entityAlias="SI" fieldAlias="description"/>
<moqui.entity.view.DbViewEntityAlias entityAlias="SI" fieldAlias="sequenceNum"/>
<moqui.entity.view.DbViewEntityAlias entityAlias="ST" fieldAlias="typeDescription" fieldName="description"/>
</moqui.entity.view.DbViewEntity>
可以看到实体和字段名与XML元素和属性名相关。要使用这些实体,只要执行它们,就像任意其他实体一样。
动态视图实体
即使EntityFacade在查找过程中有自动视图视图最小化,仍有些场景需要或想用程序构建即时生成一个视图,而不时静态定义一个视图实体。
要这么做,使用 EntityFind.makeEntityDynamicView() 方法获取一个 EntityDynamicView 接口的实例。此接口上有方法做与静态视图实体中的XML元素一样的事情。使用 addMemberEntity(String entityAlias, String entityName, String joinFromAlias, Boolean joinOptional, Map
在静态(XML定义的)视图实体中没有的一个方便的选项是基于关系定义联结入一个成员实体。使用 addRelationshipMember(String entityAlias, String joinFromAlias, String relationshipName, Boolean joinOptional) 方法。
给字段设别名使用addAlias(String entityAlias, String name, String field, String function) 方法,简化版有addAlias(String entityAlias, String name)或addAliasAll(String entityAlias, String prefix) 方法。
你可以可选地用setEntityName()方法为动态视图指定一个名字,但是一般这在大多数情况下用于调试,默认的名字(DynamicView)一般就好了。
一旦完成这些,就在你用来创建 EntityDynamicView 对象的 EntityFind 对象上和平常一样,指定条件并执行查找操作。