使用ORM提取数据很容易! 是吗?

介绍

几乎任何系统都以某种方式与外部数据存储一起运行。 在大多数情况下,它是一个关系数据库,并且数据获取通常委托给某些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的生存时间将与RD​​BMSes一样长。

在CUBA框架中,我们试图简化ORM的使用,以使开发人员尽可能地轻松。 在下一版本中,我们将引入更多更改。 我不确定这些接口是视图接口还是其他接口,但是我可以肯定的是,使用CUBA在下一版本中使用ORM将得到简化。

翻译自: https://www.javacodegeeks.com/2019/09/fetching-data-with-orm-easy.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值