java orm 开源框架
重要要点
- Reladomo是由高盛开发的企业级Java ORM,并于2016年作为开源项目发布。
- Reladomo提供了许多独特而有趣的功能,例如强类型查询语言,分片,时间支持,实际可测试性和高性能缓存。
- Reladomo是一个自以为是的框架,基于指导其发展的一系列核心价值观。
- 本文中的示例说明了Reladomo的可用性和可编程性。
早在2004年,我们就面临着艰巨的挑战。 我们需要一种方法来抽象出Java应用程序中的数据库交互,而该交互不适合任何现有框架。 该应用程序具有以下常规解决方案之外的需求:
- 数据被高度分割。 有100多个具有相同模式但数据不同的数据库。
- 数据是基于时间的(我们将在本文的第2部分中对此进行解释,敬请期待!)。
- 针对数据的查询不一定是静态的,某些查询必须从一组复杂的用户输入中动态创建。
- 数据模型相当复杂-数百张表。
我们从2004年开始Reladomo的开发。那年晚些时候进行了首次生产部署,此后一直定期发布。 在随后的几年中,Reladomo已被高盛(Goldman Sachs)广泛采用,并且使用它的应用程序指导了我们添加的主要新功能。 现在,它已用于多种分类帐,中层办公室贸易处理,资产负债表处理以及许多其他应用程序。 高盛(Goldman Sachs)在2016年根据Apache 2.0许可发布了Reladomo(关系域对象的缩写)作为开源项目。
为什么要建立另一个ORM?
很简单,现有解决方案无法满足我们的核心要求,而传统的ORM存在需要解决的问题。
我们决定消除代码级样板和痕迹结构。 在Reladomo中,没有用于获取,关闭,泄漏或冲洗的连接。 没有会议。 没有EntityManager。 没有LazyInitializationException。 API以两种基本方式提供:在域对象本身上,并通过强类型List实现高度增强。
Reladomo查询语言对我们来说是另一个关键点。 基于字符串的语言不适合我们的应用程序,也通常不适合面向对象的代码。 除了最琐碎的查询之外,将字符串连接在一起以形成动态查询是行不通的。 基于字符串连接来维护这些动态查询是一项令人沮丧的工作。
分片是我们需要完整的本机支持的另一个领域。 Reladomo中的分片非常灵活,可以处理出现在不同分片中,指向不同对象的相同主键值。 分片查询语法自然适合查询语言。
时态(单时态和双时态)支持可以帮助数据库设计人员记录和推理有关时态的信息,Richard Snodgrass在他的书《用SQL开发面向时间的数据库应用程序》中描述的是Reladomo的真正独特功能。 它适用于许多地方,从各种会计系统到参考数据,到需要完全可重复性的任何地方。 即使是简单的应用程序(例如项目协作工具),也可以从单时间表示中受益,使用户界面可以像时光机一样工作,并显示事物的变化。
真正的可测试性在所有要做的事情上都很高,我们很早就决定,做到这一点的唯一方法就是自己做饭:绝大多数Reladomo测试都是使用Reladomo自己的测试实用程序编写的! 我们有务实的测试观点。 我们希望测试可以增加长期价值。 Reladomo测试易于设置,并且可以针对内存数据库执行所有生产代码,从而可以进行连续集成测试。 这些测试可帮助开发人员了解与数据库的交互,而无需使用已安装的数据库配置开发环境。
最后,我们不想在性能上有所妥协。 Reladomo最重要,技术上最复杂的部分之一是它的缓存。 这是一个无键,多索引,事务性对象缓存。 将对象作为对象缓存,并确保其数据占用单个内存引用。 对象缓存由引用相同对象的查询缓存增强。 查询缓存很聪明-它不会返回陈旧的结果。 当多个JVM使用Reladomo向同一数据写入时,缓存可以正常工作。 可以在启动时将其配置为按需缓存或完整缓存。 对于正确的数据和应用程序,甚至可以将对象存储在堆外,以便通过复制进行大规模缓存。 我们正在生产中运行的缓存超过200GB。
原则发展
Reladomo被定位为框架,而不是库。 框架超越了库所提供的功能,它通过规定和考虑哪些编码模式合适以及哪些不合适。 Reladomo还会生成代码,并且所生成的API有望在其余的代码中广泛使用。 因此,当务之急是,框架代码和应用程序代码必须具有统一的观点。
我们定义我们的核心价值,以便我们的潜在用户可以确定Reladomo是否适合他们:
- 目标代码要在生产中运行多年甚至数十年。
- 不要重复自己。
- 使代码更改变得容易。
- 以基于域的面向对象的方式编写代码。
- 不要妥协正确性和一致性。
这些核心价值观及其后果在我们的《 哲学与愿景》文档中进行了详细说明。
可用性和可编程性
我们将使用几个小型领域模型来演示Reladomo的一些功能。 首先,关于宠物的非时间模型:
第二,教科书分类帐的模型:
在此模型中,帐户交易证券(产品),并且该产品具有任意数量的标识符(称为同义词)。 累计余额保留在余额对象中。 余额可以表示有关该帐户的任意数量的累计值,例如数量,应纳税所得额,利息等。您可以在github上查看这些模型的代码 。
我们将很快看到,这是一个双时态模型的示例。 现在,我们将忽略时间位,它们不会妨碍您。
通过为每个概念对象创建Reladomo对象定义并使用它们生成一些类来定义模型。 我们希望您定义的域类可以用作您的真实业务域。 初始生成后,域中的具体类将永远不会被覆盖。 每当模型或Reladomo版本更改时,都会生成它们的抽象超类。 您可以-并且应该-将方法添加到这些具体类中,然后将其检入版本控制系统中。
Reladomo提供的大多数API都在生成的类上:我们的pet示例中为PetFinder, PetAbstract
和PetListAbstract
。 PetFinder
具有常规的get / set方法和其他一些用于持久性的方法。 API真正有趣的部分位于Finder和List上。
顾名思义,特定于类的Finder(例如PersonFinder)用于查找事物。 这是一个简单的示例:
Person john = PersonFinder.findOne(PersonFinder.personId().eq(8));
请注意,没有要获取和关闭的连接或会话。 在所有上下文中,检索到的对象都是有效的引用。 您可以自由地将其传递给不同的线程,并使它参与事务性工作单元。 如果返回多个对象,则findOne会引发异常。
让我们分解一下这个表达式。 PersonFinder. firstName ()
PersonFinder. firstName ()
是Attribute
。 它是类型化的(它是一个StringAttribute
):您可以调用rstName
().eq( "John" )
,但是不能firstName ().eq(8)
或firstName ().eq(someDate)
。 它还具有在其他类型的属性上找不到的特殊方法,例如:
PersonFinder.firstName().toLowerCase().startsWith("j")
如T方法oLowerCase(), startsWith()
和其他许多人不提供比方说,一个IntegerAttribute
,它有自己的一套专门的方法。
所有这些都创建了两个重要的可用性点:首先,您的IDE可以帮助您编写正确的代码。 其次,当您对模型进行更改时,编译器将找到所有需要更改的位置。
属性上具有创建操作的方法,例如eq(), greaterThan()
等。Reladomo中的操作用于通过Finder. findOne or Finder. findMany
检索对象Finder. findOne or Finder. findMany
Finder. findOne or Finder. findMany
Finder. findOne or Finder. findMany
。 操作实现是不变的。 它们可以与and()
和or()
组合在一起:
Operation op = PersonFinder.firstName().eq("John");
op = op.and(PersonFinder.lastName().endsWith("e"));
PersonList johns = PersonFinder.findMany(op);
执行大量IO的应用程序倾向于批量加载数据。 这可能意味着使用条款。 如果我们构造此操作:
Set<String> lastNames = ... // a large set, say 10K elements
PersonList largeList =
PersonFinder.findMany(PersonFinder.lastName().in(lastNames));
在后台,Reladomo分析您的Operation
并生成相应SQL。 对于大型子句将生成什么sql? 在Reladomo中,答案是:“取决于”。 Reladomo可以选择发出多个从句,或者根据目标数据库使用临时表联接。 从用户角度看,该选择是透明的。 Reladomo的实现将根据操作和数据库有效地返回正确的结果。 如果配置发生更改,开发人员不必做出必然会出错的选择,也不必编写复杂的代码来应对变化。 附送电池 !
主键
Reladomo中的主键是对象属性的任意组合。 无需定义键类或将这些属性区别对待。 我们的理念是复合键在所有模型中都是非常自然的,使用它们应该没有障碍。 在我们的简单交易模型中, ProductSynonym
类具有自然的组合键:
<Attribute name="productId"
javaType="int"
columnName="PRODUCT_ID"
primaryKey="true"/>
<Attribute name="synonymType"
javaType="String"
columnName="SYNONYM_TYPE"
primaryKey="true"/>
当然,合成键在某些情况下很有用。 我们支持使用基于表的高性能方法来生成合成密钥。 合成密钥是按批,异步和按需生成的。
人际关系
类之间的关系在模型中定义:
<Relationship name="pets"
relatedObject="Pet"
cardinality="one-to-many"
relatedIsDependent="true"
reverseRelationshipName="owner">
this.personId = Pet.personId
</Relationship>
定义关系提供了三种读取功能:
- 对象上的get方法,如果通过reverseRelationshipName属性将关系标记为双向,则可能使用相关对象上的get方法,例如
person.getPets()
- 导航查找器上的关系,例如
PersonFinder. pets ()
PersonFinder. pets ()
。 - 能够在每个查询的基础上深度获取关系。
深度获取是一种有效地检索相关对象的能力,可以避免众所周知的N+1 query problem
。 如果检索某些人对象,则可以要求有效地加载他们的宠物对象。
PersonList people = ...
people.deepFetch(PersonFinder.pets());
或更有趣的例子:
TradeList trades = ...
trades.deepFetch(TradeFinder.account()); // Fetch accounts for these trades
trades.deepFetch(TradeFinder.product()
.cusipSynonym()); // Fetch the products and the
// products’ CUSIP synonym (a type of identifier) for these trades
trades.deepFetch(TradeFinder.product()
.synonymByType("ISN")); // Also fetch the products’ ISN
// synonym (another identifier).
可以指定可达图的任何部分。 请注意,这是如何不作为模型的一部分实现的。 该模型没有“渴望”或“懒惰”的概念。 这是指定此问题的特定代码段。 因此,更改模型不可能彻底改变现有代码的IO和性能,从而使模型更加敏捷。
创建Operation
时可以使用关系:
Operation op = TradeFinder
.account()
.location()
.eq("NY"); // Find all trades
// belonging to NY accounts.
op = op.and(TradeFinder.product()
.productName()
.in(productNames)); // … and whose product name
// is included in the supplied list
TradeList trades2 = TradeFinder.findMany(op);
关系在Reladomo中没有实际引用地实现。 这使得在内存和IO方面添加关系成为免费的。
Reladomo中的关系非常灵活。 考虑一个具有许多不同类型的同义词(例如CUSIP,Ticker等)的Product对象的教科书示例。 我们已经在交易模型中定义了这个例子。 从Product
到ProductSynonym
的传统一对多关系几乎不再有用:
<Relationship name="synonyms"
relatedObject="ProductSynonym"
cardinality="one-to-many">
this.productId = ProductSynonym.productId
</Relationship>
这样做的原因是,很少要在查询中返回产品的所有同义词。 两种类型的高级关系使这个常见示例更加有用。 具有常量表达式的关系可以在模型中表示重要的业务概念。 例如,如果我们想按名称访问产品的CUSIP同义词,则添加以下关系:
<Relationship name="cusipSynonym"
relatedObject="ProductSynonym"
cardinality="one-to-one">
this.productId = ProductSynonym.productId and
ProductSynonym.synonymType = "CUS"
</Relationship>
请注意,我们如何在上面的deepFetch
和query示例中使用此cusipSynonym
关系。 这具有三个好处:首先,我们不必在代码中重复“ CUS ”。 其次,如果我们想要的只是CUSIP,我们无需支付检索所有同义词的IO成本。 第三,查询的可读性和书写习惯更加丰富。
可组合性
基于字符串的查询的最大问题之一是它们很难编写。 通过使用类型安全的,基于域的面向对象的查询语言,我们将可组合性提高到了新的水平。 为了说明这一点,让我们看一个有趣的例子。
在我们的交易模型中,交易对象和余额对象都与帐户和产品都有关系。 假设您有一个GUI,可以通过过滤帐户和产品来检索交易。 一个不同的窗口允许通过过滤帐户和产品来检索余额。 自然,因为我们要处理的是相同的实体,所以过滤器是相同的。 使用Reladomo,可以轻松地在两者之间共享代码。 我们已经将产品和帐户业务逻辑抽象为几个GUI组件类 ,然后使用它们:
public BalanceList retrieveBalances()
{
Operation op = BalanceFinder.businessDate().eq(readUserDate());
op = op.and(BalanceFinder.desk().in(readUserDesks()));
Operation refDataOp = accountComponent.getUserOperation(
BalanceFinder.account());
refDataOp = refDataOp.and(
productComponent.getUserOperation(BalanceFinder.product()));
op = op.and(refDataOp);
return BalanceFinder.findMany(op);
}
这将发出以下SQL:
select t0.ACCT_ID,t0.PRODUCT_ID,t0.BALANCE_TYPE,t0.VALUE,t0.FROM_Z,
t0.THRU_Z,t0.IN_Z,t0.OUT_Z
from BALANCE t0
inner join PRODUCT t1
on t0.PRODUCT_ID = t1.PRODUCT_ID
inner join PRODUCT_SYNONYM t2
on t1.PRODUCT_ID = t2.PRODUCT_ID
inner join ACCOUNT t3
on t0.ACCT_ID = t3.ACCT_ID
where t1.FROM_Z <= '2017-03-02 00:00:00.000'
and t1.THRU_Z > '2017-03-02 00:00:00.000'
and t1.OUT_Z = '9999-12-01 23:59:00.000'
and t2.OUT_Z = '9999-12-01 23:59:00.000'
and t2.FROM_Z <= '2017-03-02 00:00:00.000'
and t2.THRU_Z > '2017-03-02 00:00:00.000'
and t2.SYNONYM_TYPE = 'CUS'
and t2.SYNONYM_VAL in ( 'ABC', 'XYZ' )
and t1.MATURITY_DATE < '2020-01-01'
and t3.FROM_Z <= '2017-03-02 00:00:00.000'
and t3.THRU_Z > '2017-03-02 00:00:00.000'
and t3.OUT_Z = '9999-12-01 23:59:00.000'
and t3.CITY = 'NY'
and t0.FROM_Z <= '2017-03-02 00:00:00.000'
and t0.THRU_Z > '2017-03-02 00:00:00.000'
and t0.OUT_Z = '9999-12-01 23:59:00.000'
ProductComponent和AccountComponent类可完全重用于贸易(请参见BalanceWindow和TradeWindow)。 但是可组合性并不止于此。 假设业务需求已更改,并且仅对于“余额”窗口,用户希望使用适合帐户筛选器或产品筛选器的余额。 使用Reladomo,那将是一行代码更改:
refDataOp = refDataOp.or(
productComponent.getUserOperation(BalanceFinder.product()));
发出SQL现在非常不同:
select t0.ACCT_ID,t0.PRODUCT_ID,t0.BALANCE_TYPE,t0.VALUE,t0.FROM_Z,
t0.THRU_Z,t0.IN_Z,t0.OUT_Z
from BALANCE t0
left join ACCOUNT t1
on t0.ACCT_ID = t1.ACCT_ID
and t1.OUT_Z = '9999-12-01 23:59:00.000'
and t1.FROM_Z <= '2017-03-02 00:00:00.000'
and t1.THRU_Z > '2017-03-02 00:00:00.000'
and t1.CITY = 'NY'
left join PRODUCT t2
on t0.PRODUCT_ID = t2.PRODUCT_ID
and t2.FROM_Z <= '2017-03-02 00:00:00.000'
and t2.THRU_Z > '2017-03-02 00:00:00.000'
and t2.OUT_Z = '9999-12-01 23:59:00.000'
and t2.MATURITY_DATE < '2020-01-01'
left join PRODUCT_SYNONYM t3
on t2.PRODUCT_ID = t3.PRODUCT_ID
and t3.OUT_Z = '9999-12-01 23:59:00.000'
and t3.FROM_Z <= '2017-03-02 00:00:00.000'
and t3.THRU_Z > '2017-03-02 00:00:00.000'
and t3.SYNONYM_TYPE = 'CUS'
and t3.SYNONYM_VAL in ( 'ABC', 'XYZ' )
where ( ( t1.ACCT_ID is not null )
or ( t2.PRODUCT_ID is not null
and t3.PRODUCT_ID is not null ) )
and t0.FROM_Z <= '2017-03-02 00:00:00.000'
and t0.THRU_Z > '2017-03-02 00:00:00.000'
and t0.OUT_Z = '9999-12-01 23:59:00.000'
请注意,此SQL与先前SQL在结构上有所不同。 需求从“和”更改为“或”,我们将代码从“和”更改为“或”,并且可以正常工作。 包括电池! 如果使用基于字符串的查询或公开“联接”的任何查询机制来实现,则需要将需求从“和”更改为“或”。
CRUD和工作单位
Reladomo的CRUD API在对象和列表实现中。 该对象具有诸如insert()和delete()之类的方法,而列表具有批量方法。 没有“保存”或“更新”方法。 在持久对象上设置值将更新数据库。 预期大多数写入将在事务中执行,该事务通过命令模式实现:
MithraManagerProvider.getMithraManager().executeTransactionalCommand(
tx ->
{
Person person = PersonFinder.findOne(PersonFinder.personId().eq(8));
person.setFirstName("David");
person.setLastName("Smith");
return person;
});
UPDATE PERSON
SET FIRST_NAME='David', LAST_NAME='Smith'
WHERE PERSON_ID=8
对数据库的写入进行合并和批处理,唯一的约束是正确性。
PersonList对象具有许多有用的方法,这些方法可提供基于集合的API。 例如,您可以执行以下操作:
Operation op = PersonFinder.firstName().eq("John");
op = op.and(PersonFinder.lastName().endsWith("e"));
PersonList johns = PersonFinder.findMany(op);
johns.deleteAll();
从所有方面来看,您可能会认为这首先解析了列表,然后一个一个地删除了个人记录,但事实并非如此。 相反,它将发出以下(事务性)查询:
DELETE from PERSON
WHERE LAST_NAME like '%e' AND FIRST_NAME = 'John'
很好,但这不是真正的生产应用程序所需的唯一批量删除类型。 考虑应用程序需要清除旧数据的情况。 显然,该数据已不再使用,因此不需要在整个数据集中进行整体交易。 可能需要在后台过程中以尽力而为的方式删除数据。 为此,您可以使用:
johns.deleteAllInBatches(1000);
这会根据目标数据库发出不同类型的查询:
MS-SQL:
delete top(1000) from PERSON
where LAST_NAME like '%e' and FIRST_NAME = 'John'
PostgreSQL:
delete from PERSON
where ctid = any (array(select ctid
from PERSON
where LAST_NAME like '%e'
and FIRST_NAME = 'John'
limit 1000))
而且,它非常努力地完成工作,处理临时故障并在一切完成后返回。 这就是我们所说的“包含电池”的含义-常见的图案易于烘烤且易于烘焙。
易于整合
我们对Reladomo进行了结构设计,以使其易于与您的代码集成。
首先,Reladomo具有很少的依赖性。 在运行时,类路径上只有六个jar(主库jar和五个浅依赖性)。 对于完整的生产部署,您需要一个驱动程序类,一个slf4j日志实现和您自己的代码。 这给了您极大的自由,可以随意放入其他任何东西,而不必担心jar冲突。
第二,我们致力于在Reladomo中提供向后兼容性。 您应该能够在不破坏代码的情况下升级Reladomo的版本。 如果我们计划进行一项更改,从而导致向后不兼容的更改,那么我们将确保您至少有一年的时间才能切换到新API。
结论
尽管我们非常重视可用性(“包括电池!”),但我们认识到存在许多不同的用例,并且试图将所有人的所有东西都用不上。
困扰传统ORM的问题之一是抽象泄漏。 如果正确实施,我们的核心价值观将创建一个非常引人注目的系统,从而避免了这些渗漏的抽象。 Reladomo中没有本机查询或存储过程支持,这并非偶然。 我们非常努力地不写说明“如果基础数据库支持Y,则支持功能X”的文档。
Reladomo的功能比我们这里未介绍的要多。 随时在Github上访问我们,看看文档和Katas (我们的学习Reladomo的教程集)。 在本文的第二部分(六月),我们将展示Reladomo的一些性能,可测试性和企业功能。
java orm 开源框架