提交机制-特殊古巴魔术

气道正压通气


视图(或多个视图)是CUBA平台的概念之一,并不是Web框架世界中最常见的概念。 理解它意味着在由于数据加载不完整而导致应用程序突然停止工作时,避免出现愚蠢的错误。 让我们看看什么表示(双关语意图)以及为什么它实际上很方便。


数据卸载问题


让我们更简单地讨论主题领域,并使用其示例来考虑问题。 假设我们有一个Customer实体,它以多对一关系引用一个CustomerType实体,换句话说,买方具有一个指向描述它的特定类型的链接:例如,“摇钱树”,“快照程序”等。 CustomerType实体具有名称属性,其中存储了类型名称。


而且,大概,CUBA中的所有新来者(甚至是高级用户)迟早都会收到此错误:


IllegalStateException: Cannot get unfetched attribute [type] from detached object com.rtcab.cev.entity.Customer-e703700d-c977-bd8e-1a40-74afd88915af [detached].


在CUBA UI中无法获取未提取的属性错误


承认,您还亲眼看到了吗? 我-是的,在一百种不同的情况下。 在本文中,我们将研究此问题的原因,根本原因以及解决方法。
对于初学者,请先简要介绍一下视图的概念。


什么是观点?


CUBA中的视图本质上是数据库中列的集合,必须在单个查询中将它们一起加载。


假设我们要创建一个带有客户表的UI,其中第一列是客户的名称,第二列是customerType属性的类型名称(如上面的屏幕截图所示)。 逻辑上假设,在此数据模型中,数据库中将有两个单独的表,一个用于Customer实体,另一个用于CustomerTypeSELECT * from CEV_CUSTOMERSELECT * from CEV_CUSTOMER将仅从一个表(属性name等)返回我们的数据。 显然,为了也从其他表中获取数据,我们将使用JOIN。


在通过JOIN使用经典SQL查询的情况下,关联的层次结构(引用属性)从图扩展为平面列表。
译者注:换句话说,表之间的关系被删除,结果以表示表联合的单个数据数组的形式显示。


对于CUBA,使用ORM,它不会丢失有关实体之间关系的信息,并以所请求数据的整数图的形式显示查询结果。 在这种情况下,将JPQL(一种SQL的对象类似物)用作查询语言。


尽管如此,仍然需要以某种方式从数据库中卸载数据并将其转换为实体图。 为此,对象关系映射机制(JPA是)具有两种主要方法来查询数据库。


延迟加载与 渴望获取


延迟加载和贪婪加载是从数据库获取数据的两种可能的策略。 两者之间的根本区别是加载链接表中的数据的时间。 一个小例子,可以更好地理解:


还记得《霍比特人》(Hobbit or Round Trip)一书中的场景吗?在那本书中,甘道夫(Gandalf)和比尔博(Bilbo)的一群侏儒试图在Beorn家中过夜。 甘道夫命令矮人严格轮流出现,只有在他仔细地与Beorn达成协议并开始一次提出之后,才开始出现,以免主人一次容纳15位客人而震惊。


甘道夫和比恩的小矮人


因此,甘道夫(Gandalf)和Beorn家中的侏儒...考虑懒惰和贪婪的下载时,这可能不是想到的第一件事,但是肯定有相似之处。 甘道夫在这里采取了明智的行动,因为他意识到局限性。 可以说他有意识地选择了gnome的延迟加载,因为他知道立即下载所有数据对于该数据库来说是一项繁重的操作。 但是,在第8位侏儒之后,甘道夫转而贪婪地加载并加载了许多剩余的侏儒,因为他注意到对数据库的访问过于频繁开始使她感到不安。


道德是懒惰和贪婪的加载都有其优点和缺点。 您可以决定在每种特定情况下要应用的内容。


请求问题N +1


如果您随便使用延迟加载,都会经常出现N + 1查询问题。 为了说明,让我们看一段Grails代码。 这并不意味着在Grails中所有内容都会延迟加载(实际上,您可以自己选择启动方法)。 在Grails中,默认情况下,对数据库的查询将返回具有表中所有属性的实体实例。 本质上,执行SELECT * FROM Pet


如果您想深入了解实体之间的关系,则必须在事后进行。 这是一个例子:


 function getPetOwnerNamesForPets(String nameOfPet) { def pets = Pet.findAll(sort:"name") { name == nameOfPet } def ownerNames = [] pets.each { ownerNames << it.owner.name } return ownerNames.join(", ") } 

该图在这里通过一行it.owner.nameit.owner.name 。 所有者是原始请求( Pet.findAll )中未加载的关系。 因此,每次调用此行时,GORM都会执行类似SELECT * FROM Person WHERE id='…' 。 纯净水懒加载。


如果计算SQL查询的总数,则得到N(每个it.owner调用一个所有者)+1(原始Pet.findAll )。 如果您想更深入地了解相关实体的图,则数据库很可能会很快找到其限制。


作为开发人员,您不太可能注意到这一点,因为从您的角度来看,您只是绕过对象图。 一小行中的这种隐藏嵌套导致数据库真正的痛苦,并且有时使惰性加载变得危险。


发展一个爱好类比,N + 1的问题可能表现如下:想象甘道夫不能在他的记忆中存储侏儒的名字。 因此,一一介绍矮人,他被迫退回到他的团队,并向矮人询问他的名字。 有了这些信息,他就可以回到Beorn并代表Thorin。 然后,他为Bifur,Bofur,Fili,Kili,Dori,Nori,Ori,Oin,Gloyn,Balin,Dvalin和Bombur重复此操作。


矮人N + 1问题


不难想象,这种情况不太可能发生:什么接收者想要等待这么长时间的请求信息? 因此,您不应该无所顾忌地使用这种方法,而盲目地依赖于持久性映射器的默认设置。


使用CUBA视图解决N + 1个查询的问题


在CUBA中,您很可能永远不会遇到N + 1查询问题,因为该平台决定根本不使用隐藏的延迟加载。 相反,CUBA引入了表示的概念。 视图是对应选择哪些属性以及将其与实体实例一起加载的描述。 像


 SELECT pet.name, person.name FROM Pet pet JOIN Person person ON pet.owner == person.id 

一方面,该视图描述了需要从主表( Pet )加载的列(而不是通过*加载所有属性),另一方面,该视图描述了应从c-JOIN表加载的列。


您可以将CUBA视图想象为OR-Mapper的SQL视图:操作原理大致相同。


在CUBA平台中,如果不使用视图,则无法通过DataManager调用查询。 该文档提供了一个示例:


 @Inject private DataManager dataManager; private Book loadBookById(UUID bookId) { LoadContext<Book> loadContext = LoadContext.create(Book.class) .setId(bookId).setView("book.edit"); return dataManager.load(loadContext); } 

在这里,我们要按书名下载该书。 在创建Load上下文时, setView("book.edit")方法指示应从数据库中加载图书的视图。 如果您不传递任何视图,则数据管理器将使用每个实体具有的三个标准视图之一: _local视图 。 这里的局部是指不引用其他表的属性,一切都很简单。


通过视图解决IllegalStateException的问题


既然我们对表示的概念有了一点了解,那么让我们从文章开头回到第一个示例,并尝试防止引发异常。


消息IllegalStateException:无法从分离的对象中获取未提取的属性[]仅表示您正在尝试显示一些不包含在加载实体的视图中的属性。


如您所见,在浏览屏幕描述符中,我使用了_local view ,这就是整个问题:


 <dsContext> <groupDatasource id="customersDs" class="com.rtcab.cev.entity.Customer" view="_local"> <query> <![CDATA[select e from cev$Customer e]]> </query> </groupDatasource> </dsContext> 

要消除该错误,您首先需要在视图中包括客户类型。 由于我们无法更改_local的默认视图, 因此我们可以创建自己的视图。 在Studio中,例如,可以按以下步骤完成(右键单击实体>创建视图):


在Studio中创建视图


要么直接在我们应用程序的views.xml描述符中:


 <view class="com.rtcab.cev.entity.Customer" extends="_local" name="customer-view"> <property name="type" view="_minimal"/> </view> 

之后,我们将链接更改为浏览屏幕中的视图,如下所示:


 <groupDatasource id="customersDs" class="com.rtcab.cev.entity.Customer" view="customer-view"> <query> <![CDATA[select e from cev$Customer e]]> </query> </groupDatasource> 

这样就完全解决了问题,现在链接数据显示在客户查看屏幕中。


_最小视图和实例名称


在视图的上下文中值得一提的是_minimal视图。 本地视图的定义非常明确:它包含实体的所有属性,这些属性是表的直接属性(不是外键)。


最小表示的定义不是很明显,但是也很清楚。


CUBA具有实体实例名称的概念-实例名称。 实例名称与旧的Java中的toString()方法等效。 这是实体的字符串表示形式,用于在UI上显示和在链接中使用。 使用NamePattern实体的注释设置实例名称。


它的用法如下: @NamePattern("%s (%s)|name,code") 。 我们有两个结果:


实例名称定义了实体到UI的映射


首先,实例名称决定了如果一个实体引用另一个实体(因为Customer引用CustomerType ),那么将在UI中以什么顺序显示。


在我们的案例中,客户类型将显示为CustomerType实例的名称,并将代码添加到其中。 如果未设置实例名称,则将显示实体类的名称和特定实例的ID-同意这根本不是用户希望看到的。 有关这两种情况的示例,请参见下面的屏幕截图之前和之后。


引用未为其指定实例名称的实体


具有给定实例名称的实体引用


实例名称定义了最小视图属性


NamePattern注释影响的第二件事是:在竖线之后指定的所有属性将自动形成_minimal视图。 乍一看,这似乎很明显,因为某种形式的数据需要在UI中显示,这意味着您必须首先从数据库下载数据。 虽然,老实说,我很少考虑这个事实。


重要的是要注意,最小表示法如果与本地表示法相比可能包含对其他实体的引用。 例如,对于上面示例中的买方,我设置了一个实例名称,其中包括Customer实体的一个本地属性( name )和一个引用属性( type ):


 @NamePattern("%s - %s|name,type") 

最小表示可以递归使用:(客户[实例名称]-> CustomerType [实例名称])


译者注:自从文章发表以来,出现了另一个系统视图_base视图,其中包括所有本地非系统属性和@NamePattern批注中指定的属性(即,实际上_minimal + _local )。


结论


总之,我们总结了最重要的主题。 多亏了这些视图,在CUBA中,我们可以显式指示应从数据库中加载的内容。 视图确定将贪婪地加载什么,而其他大多数框架则秘密执行延迟加载。


代表制似乎是一个繁琐的机制,但从长远来看,它们可以证明自己是合理的。


我希望我能够以一种易于理解的方式来解释这些神秘的观点到底是什么。 当然,有更高级的使用场景,以及使用一般表示形式(特别是使用最小表示形式)时的陷阱,但是我将在另一篇文章中对此进行写作。

Source: https://habr.com/ru/post/zh-CN425765/


All Articles