介绍
几乎任何系统都以某种方式与外部数据存储一起运行。 在大多数情况下,它是一个关系数据库,并且数据获取通常委托给某些ORM实现。 ORM涵盖了很多例程,并带来了一些新的抽象作为回报。
Martin Fowler写了一篇有关ORM的有趣文章 ,其中的主要思想之一是:“ ORM帮助我们处理大多数企业应用程序中非常实际的问题。 …他们不是很好的工具,但是他们解决的问题也不是很可爱。 我认为他们应该得到更多的尊重和更多的理解。”
在CUBA框架中,我们非常频繁地使用ORM,并且由于它在世界范围内有各种各样的项目,所以对它的局限性了解很多。 有很多事情可以讨论,但我们将集中讨论其中之一:懒惰与急切的数据获取。 我们将讨论数据获取的不同方法(主要是在JPA API和Spring中),我们如何在CUBA中处理数据以及我们为提高CUBA中的ORM层所做的RnD工作。 我们将研究一些基本要素,这些要素可能会帮助开发人员避免使用ORM带来糟糕的性能问题。
取数据:懒惰还是渴望?
如果您的数据模型仅包含一个实体,那么使用ORM不会有任何问题。 让我们看一个例子。 我们有一个具有ID和名称的用户:
public class User {
@Id
@GeneratedValue
private int id;
private String name;
//Getters and Setters here }
要获取它,我们只需要很好地询问EntityManager即可:
EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User. class , id);
当实体之间存在一对多关系时,事情就会变得有趣起来:
public class User {
@Id
@GeneratedValue
private int id;
private String name;
@OneToMany
private List<Address> addresses;
//Getters and Setters here }
如果要从数据库中获取用户记录,则会出现一个问题:“我们也应该获取地址吗?”。 正确的答案将是:“取决于”。 在某些用例中,我们可能需要其中一些地址-不需要。 通常,ORM提供两个用于获取数据的选项:惰性和渴望。 它们中的大多数默认情况下都设置了惰性提取模式。 而当我们编写以下代码时:
EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User. class , 1 ); em.close(); System.out.println(user.getAddresses().get( 0 ));
我们得到了所谓的“LazyInitException”
,这使ORM新手非常困惑。 在这里,我们需要解释“附加”和“分离”对象上的概念,并讲述数据库会话和事务。
然后,将一个实体实例附加到会话,这样我们就可以获取详细信息属性。 在这种情况下,我们遇到了另一个问题–交易时间越来越长,因此陷入僵局的风险增加了。 而且,由于短查询的数量增加,将我们的代码拆分为一系列短事务可能会导致数据库“百万蚊子死亡”。
如前所述,您可能需要也可能不需要获取Addresses属性,因此仅在某些用例中需要“触摸”集合,从而添加更多条件。 嗯... 看起来越来越复杂。
好的,另一种提取类型会有所帮助吗?
public class User {
@Id
@GeneratedValue
private int id;
private String name;
@OneToMany (fetch = FetchType.EAGER)
private List<Address> addresses;
//Getters and Setters here }
好吧,不完全是。 我们将摆脱烦人的懒惰init异常,并且不应该检查实例是已附加还是已分离。 但是我们遇到了性能问题,因为同样,我们并不需要所有情况的地址,而是始终选择它们。 还有其他想法吗?
Spring JDBC
一些开发人员对ORM感到非常恼火,以至于他们使用Spring JDBC切换到“半自动”映射。 在这种情况下,我们为唯一的用例创建唯一的查询,并返回包含仅对特定用例有效的属性的对象。
它给了我们极大的灵活性。 我们只能得到一个属性:
String name = this .jdbcTemplate.queryForObject(
"select name from t_user where id = ?" ,
new Object[]{1L}, String. class );
或整个对象:
User user = this .jdbcTemplate.queryForObject(
"select id, name from t_user where id = ?" ,
new Object[]{1L},
new RowMapper<User>() {
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setName(rs.getString( "name" ));
user.setId(rs.getInt( "id" ));
return user;
}
});
您也可以使用ResultSetExtractor来获取地址,但是它涉及编写一些额外的代码,并且您应该知道如何编写SQL联接以避免n + 1 select问题 。
好吧,它又变得越来越复杂。 您可以控制所有查询并可以控制映射,但是您必须编写更多代码,学习SQL并知道如何执行数据库查询。 尽管我认为了解SQL基础知识对于几乎每个开发人员都是必不可少的技能,但其中一些人并不这么认为,因此我不会与他们争论。 如今,了解x86汇编器也不是每个人都至关重要的技能。 让我们考虑一下如何简化开发。
JPA实体图
让我们退后一步,尝试了解我们将要实现什么? 似乎我们需要做的就是准确告诉我们要在不同用例中获取哪些属性。 那我们做吧! JPA 2.1引入了新的API –实体图。 该API背后的想法很简单–您只需编写一些注释来描述应获取的内容。 让我们看一个例子:
@Entity @NamedEntityGraphs ({
@NamedEntityGraph (name = "user-only-entity-graph" ),
@NamedEntityGraph (name = "user-addresses-entity-graph" ,
attributeNodes = { @NamedAttributeNode ( "addresses" )})
}) public class User {
@Id
@GeneratedValue
private int id;
private String name;
@OneToMany (fetch = FetchType.LAZY)
private Set<Address> addresses;
//Getters and Setters here }
对于这个实体,我们描述了两个实体图– user-only-entity-graph
不获取Addresses
属性(标记为惰性),而第二个图则指示ORM选择地址。 如果我们将属性标记为渴望,则实体图设置将被忽略,并且将获取该属性。
因此,从JPA 2.1开始,您可以通过以下方式选择实体:
EntityManager em = entityManagerFactory.createEntityManager(); EntityGraph graph = em.getEntityGraph( "user-addresses-entity-graph" ); Map<String, Object> properties = Map.of( "javax.persistence.fetchgraph" , graph); User user = em.find(User. class , 1 , properties); em.close();
这种方法极大地简化了开发人员的工作,无需“接触”惰性属性并创建长事务。 很棒的事情是,实体图可以应用于SQL生成级别,因此不会从数据库中获取额外的数据到Java应用程序。 但是仍然有一个问题。 我们不能说获取了哪些属性,哪些没有。 有一个API,可以使用PersistenceUnit
类检查属性:
PersistenceUtil pu = entityManagerFactory.getPersistenceUnitUtil(); System.out.println( "User.addresses loaded: " + pu.isLoaded(user, "addresses" "User.addresses loaded: " + pu.isLoaded(user, "addresses" ));
但这很无聊。 我们可以简化一下,只是不显示未提取的属性吗?
Spring预测
Spring Framework提供了一个很棒的工具,称为Projections (它与Hibernate的Projections不同)。 如果我们只想获取实体的某些属性,则可以指定一个接口,Spring将从数据库中选择接口“实例”。 让我们看一个例子。 如果我们定义以下接口:
interface NamesOnly {
String getName(); }
然后定义一个Spring JPA存储库以获取我们的User实体:
interface UserRepository extends CrudRepository<User, Integer> {
Collection<NamesOnly> findByName(String lastname); }
在这种情况下,调用findByName方法后,我们将无法访问未提取的属性! 同样的原则也适用于详细实体类。 因此,您可以通过这种方式获取主记录和明细记录。 此外,在大多数情况下,Spring会生成“适当的” SQL,并且仅获取投影中指定的属性,即,投影的工作方式类似于实体图描述。
这是一个非常强大的概念,您可以使用SpEL表达式,使用类而不是接口等。如果您有兴趣,可以在文档中查看更多信息。
投影的唯一问题是在幕后将它们实现为地图,因此是只读的。 因此,考虑到您可以为投影定义setter方法,则既不能使用CRUD存储库也不能使用EntityManager保存更改。 您可以将投影视为DTO,并且必须编写自己的DTO到实体的转换代码。
CUBA实施
从CUBA框架开发的开始,我们就尝试优化可与数据库一起使用的代码。 在框架中,我们使用EclipseLink来实现数据访问层API。 关于EclipseLink的好处-它从一开始就支持部分实体加载,这就是为什么我们首先选择它而不是Hibernate的原因。 在此ORM中,您可以指定在JPA 2.1成为标准之前应确切加载哪些属性。 因此,我们将类似内部“实体图”的概念添加到我们的框架CUBA Views中 。 视图非常强大-您可以扩展它们,合并等等。CUBA视图创建背后的第二个原因-我们想使用短事务,并专注于主要处理分离对象,否则,我们将无法快速,快速地响应丰富的Web UI 。
在CUBA视图中,描述存储在XML文件中,如下所示:
<view class = "com.sample.User"
extends = "_local"
name= "user-minimal-view" >
<property name= "name" />
<property name= "addresses"
view= "address-street-only-view" />
</property> </view>
该视图指示CUBA DataManager提取具有其本地名称属性的User实体,并应用地址仅街道视图来获取地址,同时在查询级别获取它们(重要!)。 定义视图后,可以使用DataManager类将其应用于获取实体:
List<User> users = dataManager.load(User. class ).view( "user-edit-view" ).list();
它的工作原理很像,并且由于不加载未使用的属性而节省了大量网络流量,但是像在JPA Entity Graph中一样,存在一个小问题:我们无法说出用户实体的哪些属性已加载。 在CUBA中,我们有令人讨厌的“IllegalStateException: Cannot get unfetched attribute [...] from detached object”
。 像在JPA中一样,您可以检查是否未提取属性,但是为每个要提取的实体编写这些检查是一项无聊的工作,并且开发人员对此不满意。
CUBA View Interfaces PoC
如果我们能充分利用两个世界的优势,该怎么办? 我们决定使用Spring的方法来实现所谓的实体接口,但是这些接口在应用程序启动期间会转换为CUBA视图,然后可以在DataManager中使用。 这个想法很简单:定义一个指定实体图的接口(或一组接口)。 它看起来像Spring Projections,并且像Entity Graph一样工作:
interface UserMinimalView extends BaseEntityView<User, Integer> {
String getName();
void setName(String val);
List<AddressStreetOnly> getAddresses();
interface AddressStreetOnly extends BaseEntityView<Address, Integer> {
String getStreet();
void setStreet(String street);
} }
请注意,如果仅在一种情况下使用,则AddressStreetOnly
接口可以嵌套。
在CUBA应用程序启动期间(实际上,大多数情况是Spring Context初始化),我们为CUBA视图创建了程序化表示并将其存储在Spring上下文中的内部存储库bean中。
之后,我们需要调整DataManager,以便它除了可以接受CUBA View字符串名称之外,还可以接受类名称,然后我们只需传递接口类即可:
List<User> users = dataManager.loadWithView(UserMinimalView. class ).list();
我们为每个从数据库获取的实例生成代理来实现实体视图,就像冬眠一样。 而且,当您尝试获取属性的值时,代理会将调用转发给真实实体。
通过这种实现,我们试图用一块石头杀死两只鸟:
- 接口中未声明的数据不会加载到Java应用程序代码中,从而节省了服务器资源
- 开发人员仅使用获取的属性,因此不会再出现“ UnfetchedAttribute”错误(在Hibernate中也称为
LazyInitException
)。
与Spring Projections相比,实体视图包装实体并实现CUBA的实体接口,因此可以将它们视为实体:您可以更新属性并将更改保存到数据库。
这里的“第三只鸟” –您可以定义一个仅包含吸气剂的“只读”接口,从而完全防止实体在API级别进行修改。
另外,我们可以对分离的实体执行一些操作,例如将该用户的名称转换为小写:
@MetaProperty default String getNameLowercase() {
return getName().toLowerCase(); }
在这种情况下,所有计算出的属性都可以从实体模型中移出,因此您不必将数据获取逻辑与用例特定的业务逻辑混合在一起。
另一个有趣的机会–您可以继承接口。 这使您可以准备具有不同属性集的多个视图,然后根据需要将它们混合。 例如,您可以有一个包含用户名和电子邮件的界面,以及另一个包含名称和地址的界面。 而且,如果您需要第三个视图接口,其中应该包含名称,电子邮件和地址,则可以通过将二者结合起来来实现–这要归功于Java中接口的多重继承。 请注意,您可以将此第三个接口传递给使用第一个或第二个接口的方法,OOP原理照常在这里工作。
我们还实现了视图之间的实体转换–每个实体视图都有reload()方法,该方法接受另一个视图类作为参数:
UserFullView userFull = userMinimal.reload(UserFullView. class );
UserFullView可能包含其他属性,因此该实体将从数据库中重新加载。 实体重新加载是一个懒惰的过程,仅当您尝试获取实体属性值时才执行。 我们之所以这样做是因为在CUBA中,我们有一个“网络”模块,可呈现丰富的UI,并可能包含自定义的REST控制器。 在此模块中,我们使用相同的实体,并且可以将其部署在单独的服务器上。 因此,每个实体重新加载都会通过核心模块(aka中间件)向数据库发出附加请求。 因此,通过引入惰性实体重新加载,我们节省了一些网络流量和数据库查询。
PoC可以从GitHub下载-随时使用。
结论
ORM将在不久的将来在企业应用程序中大量使用。 我们只需要提供一些将数据库行转换为Java对象的工具即可。 当然,在复杂的高负载应用程序中,我们将继续看到独特的解决方案,但是ORM的生存时间将与RDBMSes一样长。
在CUBA框架中,我们试图简化ORM的使用,以使开发人员尽可能地轻松。 在下一版本中,我们将引入更多更改。 我不确定这些接口是视图接口还是其他接口,但是我可以肯定的是,使用CUBA在下一版本中使用ORM将得到简化。
翻译自: https://www.javacodegeeks.com/2019/09/fetching-data-with-orm-easy.html