春争日,夏争时,扫码抽取夏日礼包!!! 了解详情
写点什么

Reladomo:自备全套功能的企业级开源 Java ORM(一)

  • 2017 年 8 月 20 日
  • 本文字数:8993 字

    阅读完需:约 30 分钟

本文要点

  • Reladomo 是 Goldman Sachs 开发的企业级 Java ORM,并在 2016 年作为开源项目发布。
  • Reladomo 提供了一系列独到的和有趣的特性,包括强类型查询语言、数据分片、临时表支持、真正的可测试性以及高性能缓存。
  • Reladomo 的开发是围绕着一系列的核心价值开展的,因此是一种“自有一套”(Opinionated)的框架。
  • 本文提供了一些例子,很好地展示了 Reladomo 的可用性和可编程性。

回顾 2004 年,那时我们正面对一个艰难的挑战。我们的 Java 应用需要采用一种能抽象数据库交互的方法,任何现有的框架都无法适应该需求。该应用具有下列需求,超出了惯常解决方案所能提供的:

  • 数据是高度切分的。数据存储在一百多个具有相同模式但是数据不同的数据库中。
  • 数据是双时态(Bitemporal)的。该内容我们将在本文第二部分介绍,敬请关注!
  • 数据上的查询并非完全是静态的,有一些查询必须从一系列的复杂用户输入中动态创建。
  • 数据模型有一定程度的复杂性,具有数以百计的数据库表。

我们从 2004 年就启动了 Reladomo 的开发工作,并于当年完成了首个生产部署。此后,我们定期发布 Reladomo 版本。这些年间,Reladomo 以在 Goldman Sachs 得到了广泛采用,其中的主要新特性也是在应用的指导下添加的。Reladomo 已用于累计分类账目(multiple ledger)、中台(Middle Office)交易处理、资产负债表(Balance Sheet)处理等数十种应用中。在 2016 年,Goldman Sachs 以 Apache 2.0 许可开源发布 Reladomo。“Reladomo”是“RELAtional DOMain Objects”(关系域对象)一词的缩写。

我们为什么要再构建一个 ORM 产品?

原因十分简单,因为现有的解决方案并不能满足我们的核心需求,传统的 ORM 存在一些尚未解决的问题。

我们决定在代码这一级上消除样板(Boilerplate)和未完全发育的结构。在 Reladomo 中不存在对连接的获取、关闭、泄漏或清空,也没有 Session、EntityManager 和 LazyInitializationException。Reladomo 以两种基本方式提供 API,即在域对象本身上,以及通过强类型 List 实现的高度提升。

对于我们而言,另一个关键点是 Reladomo 的查询语言。通常,基于字符串的语言完全不适合于我们的应用及面向对象的代码。除了一些最琐碎的查询之外,通过将字符串连接在一起而构建动态查询并不会很好地工作。维护这些基于字符串连接的动态查询无疑是一件让人头疼的事情。

此外,我们需要对数据分片的完全原生支持。Reladomo 中的数据分片是非常灵活的,可以处理出现在不同分片中、指向不同对象的同一主键值。分片查询的语法天然地适合于查询语言。

正如在 Richard Snodgrass 编写的《 Developing Time-Oriented Database Applications in SQL 》一书中所指出的,对时序(单时序或双时序)的支持可以帮助数据库设计人员记录和推理时序信息,这完全是 Reladomo 独有的一个特性。该特性适用面很广,从各种各样的记账系统,到用于所有需要完全可再现性的参考数据(Reference Data)。甚至像项目协作工具这类基本应用,可以受益于单时序表示,成为一种可让用户界面能呈现事情改变方式的时间机器。

应用要实现正常的工作,真实的可测试性无疑是一件非常重要的事情。我们很早就决定,自力更生是正确做事的唯一方法。大部分 Reladomo 测试是使用 Reladomo 自身的测试工具编写的!我们对测试的看法很务实,就是要将长期价值添加到测试中。Reladomo 测试易于建立,可以在内存数据库上执行所有的生产代码,并允许持续集成测试。这些测试有助于开发人员理解与数据库的交互,无需再配置一个安装了数据库的开发环境。

最后一点,我们不想在性能上做妥协。缓存是影响性能的最重要因素之一。从技术角度看,缓存是 Reladomo 中最为复杂的部分。Reladomo 的缓存是一种无键值、多索引的事务对象缓存。对象是以对象本身的形式缓存的,并确保对象中的数据占用单一的内存引用。对象缓存还附加了对同一对象的查询缓存。查询缓存是智能的,它不会返回过期的结果。当多个 JVM 使用 Reladomo 写同一数据时,缓存会正确的工作。缓存可以被配置为在启动时按需分配,或是完全最大化。如果适当类型的数据和应用具备可复制的大规模缓存,对象甚至可以是堆外(off-heap)存储。在我们的生产系统中,就运行了超过 200GB 的缓存。

开发原则

从定位上看,Reladomo 是一个框架,而非一个软件库。要确定一个编程模式是否适用,相比于软件库,框架要更为规范,也更具有自己的一套方法。Reladomo 也会生成代码,它所生成的 API 被认识是可自由地用在开发人员代码的其余部分中。因此对于代码是否能很好地工作,框架代码和应用代码具有统一外观是势在必行的。

我们定义了以下的核心价值,这样我们的潜在用户就可据此决定 Reladomo 是否适用:

  • 代码的目标是可在生产环境中运行多年乃至数十年。
  • 一次且仅一次原则(DRY,Don’t repeat yourself)。
  • 打造易于更改的代码。
  • 以面向基于领域对象的方法编写代码。
  • 不要在正确性和一致性上妥协。

我们在“理念和愿景”文档中,详细阐述了上述核心价值及其影响。

可用性和可编程性

为展示Reladomo 的部分特性,我们将使用两个小型的领域模型。首先给出的是一个关于宠物的恒定模型(Non-temporal Model):

(点击放大图像)

第二个模型是一个出现在教科书上的分类账目模型:

(点击放大图像)

在该模型中,Account 对象交易的是有价证券(即 Product 对象),而一个 Product 对象具有多个标识码(也称为同义词)。Balance 对象中保存了累计余额信息。Balance 可以表示 Account 中任意数量的累计值,例如总量、应税收入、利息等。上述模型的代码提供于 GitHub 上。

下面我们将会看到,该例子是一个双时态模型。从现在开始,我们会忽略时态位,这并不会影响到特性展示。

在定义模型时,我们对每个概念对象创建了一个 Relamodo 对象,并使用所创建的对象去生成一些类。我们期待每个开发人员所定义的领域类都能用于真实的业务领域中。一开始生成的对象,永远不会覆写领域中的实际类。这些类的抽象超类是在每次更改模型或 Reladomo 版本后生成。开发人员也可以为这些具体的类添加一些方法,并将更改检入(check in)到版本控制系统中。

大多数 Reladomo 所提供的 API 位于生成类中。在我们提供的宠物例子中,这样的方法包括:PetFinder、PetAbstract 和 PetListAbstract。PetFinder 具有正常的 get/set 方法,以及其它一些用于持久化的方法。API 中最有意思的部分在于 Finder 和 List。

正如方法名所表示的,类特定的 Finder(例如 PersonFinder)方法是用于发现事物。下面给出一个简单的例子:

复制代码
Person john = PersonFinder.findOne(PersonFinder.personId().eq(8));

注意,这里不存在需要获取或关闭的连接和会话。方法所检索到的对象在任何场景中都是有效的引用。开发人员可将该对象传递到不同的线程中,并参与到事务的工作单元中。如果方法返回一个以上的对象,findOne 就会抛出异常。

如果我们将这个表达式进行分解。其中 PersonFinder.firstName() 是一个属性,该属性是有类型的,即 StringAttribute。开发人员可以调用 firstName().eq(“John”),而不是 firstName().eq(8) 或 firstName().eq(someDate)。该对象也具有一些在其它类型属性中无法看到的特殊方法,例如:

复制代码
PersonFinder.firstName().toLowerCase().startsWith("j")

注意,在我们所说的 IntegerAttribute 上,toLowerCase()、startsWith() 等方法是不可用的,该类型具有自身的一系列特殊方法。

上述内容给出了可用性的两个关键点。首先,所用的 IDE 应有助于开发人员去编写正确的代码。其次,在开发人员更改了模型后,编译器应可以发现所有需要更改之处。

属性具有在自身上创建操作(Operation)的方法,例如 eq()、greaterThan() 等。在 Reladomo 中,操作用于通过 Finder.findOne 或 Finder.findMany 等方法检索对象。操作的实现是不可变的,可以与 and() 和 or() 组合使用。例如:

复制代码
Operation op = PersonFinder.firstName().eq("John");
op = op.and(PersonFinder.lastName().endsWith("e"));
PersonList johns = PersonFinder.findMany(op);

在执行具有大量 IO 操作的应用时,一般应批量地加载数据。这可以使用 in 语句实现。例如,如果我们构建了如下操作:

复制代码
Set<String> lastNames = ... //lastNames 定义为一个大型集合。例如,其中包含了一万个元素。
PersonList largeList =
PersonFinder.findMany(PersonFinder.lastName().in(lastNames));

Reladomo 会在后台分析其中的操作,并生成相应的 SQL 语句。那么大型的 in 语句会生成什么样的 SQL 语句呢?对于 Reladomo,该问题的回答是:“依据情况而定”。Reladomo 可以选择发布多个 in 语句,也可以选择使用一个依赖于目标数据库的临时表连接。该选择对用户是透明的。Reladomo 的实现会根据操作和数据库的不同而有效地返回相应的正确结果,开发人员不必做选择。因为如果配置发生了更改,或是要编写复杂的代码对可变性进行处理时,开发人员所做出的选择无疑会是错误的。Reladomo 自备全套功能!

主键

Reladomo 中的主键是对象属性的任意组合。处理这些属性,无需定义键的类或是采用其它的方式。我们的理念是,组合键在所有的模型中无疑都是自然键(Natural Key),不应存在使用上的障碍。在我们给出的基本交易模型的例子中,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()。
  • 在 Finder 上浏览关系,例如,PersonFinder.pets()。
  • 根据每个查询深入获取关系的能力。

深度获取是一种高效检索相关对象的能力,避免了著名的“ N+1 查询问题”。如果要检索 Person 对象,我们可以请求高效地加载 Pet 对象,实现为:

复制代码
PersonList people = ...
people.deepFetch(PersonFinder.pets());

或者实现为更有意思的例子:

复制代码
TradeList trades = ...
trades.deepFetch(TradeFinder.account()); // 获取这些 trades 的 account 信息。
trades.deepFetch(TradeFinder.product()
.cusipSynonym()); // 获取 product,以及 product 对于 trades 的 CUSIP 同义词(是标识符的一个类型)
trades.deepFetch(TradeFinder.product()
.synonymByType("ISN")); // 同样,获取 product 的 ISN 同义词(另一个标识符)。

其中可以指定图的任何可访问部分。注意这里所使用的方法,如何避免作为模型的一部分实现。模型并不具有“eager”或“lazy”的概念,而是使用代码中的特定部分实现的。因而,更改模型不可能达到极大地改进已有代码的 IO 和性能,进而使得模型更为敏捷。

在创建 Operation 时,可以使用关系。例如:

复制代码
Operation op = TradeFinder
.account()
.location()
.eq("NY"); // 找到属于名为“NY”的 account 的所有 trades。
op = op.and(TradeFinder.product()
.productName()
.in(productNames)); // 并且 product 名字包括在给出的 TradeList 中。
TradeList trades2 = TradeFinder.findMany(op);

在 Reladomo 中,关系实现并不具有实际的引用。这使得从内存和 IO 角度看,添加关系是毫无代价的。

在 Reladomo 中,关系是非常灵活的。回到例子,Product 对象具有多个不同类型的同义词(例如,CUSIP、Ticket 等)。我们已经在 Trade 模型中定义了这个例子。Product 到 ProductSynonym 间的传统一对多关系几乎难以发挥作用:

复制代码
<Relationship name="synonyms"
relatedObject="ProductSynonym"
cardinality="one-to-many">
this.productId = ProductSynonym.productId
</Relationship>

这是因为在用户查询需要返回所有 Product 同义词的情况十分罕见。有两类高级关系会使该通用例子更为有用。一个是常量表达式,允许在模型中表示重要业务概念。例如,如果想通过名字访问 Product 的 CUSIP 同义词,我们添加了如下的关系:

复制代码
<Relationship name="cusipSynonym"
relatedObject="ProductSynonym"
cardinality="one-to-one">
this.productId = ProductSynonym.productId and
ProductSynonym.synonymType = "CUS"
</Relationship>

注意我们是如何在 deepFetch 中使用这个 cusipSynonym 关系,并在上面给出的例子进行查询。这一做法有三个好处:首先,我们不必在整个代码中重复“CUS”;其次,如果我们想要的是 CUSIP,我们不必付出 IO 上的代价去检索所有的同义词;最后,查询的可读性更好,在编写上更符合语言习惯。

可组合性

基于字符串的查询的最大问题之一是它们非常难以组合。通过具备类型安全、面向基于领域对象的查询语言,我们将可组合性带到了一个新的高度。为说明这一概念,让我们看一个有意思的例子。

对于我们的 Trade 模型,Trade 对象和 Balance 对象均具有与 Account 和 Product 同样的关系。假设在 GUI 中允许通过过滤 Account 和 Product 信息而检索 Trade,并具有提供通过过滤 Account 和 Product 信息检索 Balance 的窗口。由于要处理的是同一实体,过滤器自然是相同的。使用 Reladomo,可很容易地实现两者间的代码共用。我们已将 Product 和 Account 的业务逻辑抽象为多个 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'

对于 Trade 类,ProductComponent 类和 AccountComponen 类是完全可重用的(参见 BalanceWindow 和 TradeWindow)。但是可组合性却远非这样。如果业务需求发生了变化,假定该变化仅针对 Balance 窗口,用户想要 Balance 匹配 Account 或 Product 过滤器,那么使用 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 间的结构化差异。需求已由“and”更改为“or”,我们相应地将代码从“and”更改到“or”就良好工作了。这毫无疑问就是自带各种功能!如果使用基于字符串或是任何暴露“连接”的查询机制实现该功能,那么实现从“and”到“or”的需求更改将会更为棘手。

CRUD 和工作单元模式(Unit of Work)

Reladomo API 和 CRUD 实现在对象和列表上。对象具有 insert() 和 delete() 等方法,而列表具有批量操作方法,但是没有“save”或“update”方法。设置持久对象的将会更新数据库。我们期望大多数的写操作在事务中执行,这是通过命令行模式实现的:

复制代码
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();

从表面上看,你可能会认为上述语句会首先去解析列表,然后依次删除 Person 记录。但事实并非如此,而是发布了如下的事务查询:

复制代码
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 会尽力完成工作,处理临时失败并在完成所有任务后返回。这就是我们所说的“自备全套功能(Batteries Included)”,即集成了通用模式并简化了实现。

易于集成

我们打造了 Reladomo,并使其易于集成开发人员的代码。

首先,Reladomo 的依赖关系简单。在运行时只需在 CLASSPATH 中设置六个 jar 包(即主函数库 jar 文件及五个浅层依赖 jar 文件)。在完全生产部署时,只需要给出一个驱动类、slf4j 日志实现以及开发人员自身的代码。这将赋予开发人员非常大的自由,使得他们可以按需拉入其它所需模块,而不必担心存在 jar 冲突的问题。

其次,我们确保 Reladomo 中提供了向后兼容性。开发人员无需破坏自身代码就可以升级 Reladomo 版本。如果在我们的计划中存在可能导致向后不兼容的更改。我们将会确保开发人员至少具有一年的时间去转化到新的 API。

结论

虽然我们十分看重可使用性(即“自备全套功能!”),但是我们也知道存在着许多不同的用例,满足所有人的需求是不现实的。

一个困扰传统 ORM 的问题是抽象漏洞(Leaky Abstraction)。我们的核心价值一旦得以实现,就会创建一个能避开所有抽象漏洞的、令人惊叹的系统。Reladomo 中没有提供对查询或是存储过程的原生支持,这是刻意而为之的。我们尽量避免编写那些读上去类似于“底层数据库如果支持特性 B,那么也支持特性 A”这类的文档。

Reladomo 还具有更多的本文尚未提及的特性。敬请访问我们的 Github 代码库查看文档以及 Katas (即我们给出的一系列 Reladomo 学习教程)。本文的第二部分将于今年六月推出,其中将会展示 Reladomo 的部分性能、可测试性和一些企业特性。

作者简介

Mohammad Rezaei是 Reladomo 的主架构师,任 Goldman Sachs 公司平台业务部门的技术专家(Technology Fellow)。他具有在多种环境中编写高性能 Java 代码的丰富经验,从分区、并发交易系统,乃至采用无锁算法实现最大吞吐量的大内存系统。Mohammad 具有宾夕法尼亚大学的计算机科学本科学位,并在康奈尔大学获得物理学博士学位。

查看英文原文: Introducing Reladomo - Enterprise Open Source Java ORM, Batteries Included!

2017 年 8 月 20 日 17:397030
用户头像

发布了 227 篇内容, 共 64.7 次阅读, 收获喜欢 26 次。

关注

评论

发布
暂无评论
发现更多内容

区块链赋能生猪养殖,让“猪”事有迹可循

CECBC

强强联袂!腾讯云TDSQL与国双战略签约,锚定国产数据库巨大市场

腾讯云数据库

tdsql 国产数据库

消息队列存储消息数据设计

张靖

#架构实战营

这几个IDE是Node.js 开发人员需要知道的

@零度

node.js 前端开发

大数据开发之Hive如何提高查询效率

@零度

大数据 hive

腾讯云TDSQL数据库信创演进与实践

腾讯云数据库

tdsql 国产数据库

Linux之find exec

入门小站

WireShark好学吗?我来手把手教你学WireShark抓包及常用协议分析

学神来啦

网络安全 Wireshark 渗透测试 kali kali Linux

一文解析Apache Avro数据

华为云开发者社区

序列化 flink sql Apache Avro 反序列 Avro

一个cpp协程库的前世今生(二)协程切换的原理

SkyFire

c++ 协程 cocpp

Iog4j2漏洞相关技术分析

极光JIGUANG

forEach、map和for循环

编程江湖

大前端

☕【难点攻克技术系列】「海量数据计算系列」如何使用BitMap在海量数据中对相应的进行去重、查找和排序

浩宇天尚

BitMap bitmaps bitset 12月日更

Greenplum内核源码分析-分布式事务(一)

王凤刚(ginobiliwang)

源码分析 分布式事务 greenplum

Greenplum内核源码分析-分布式事务(二)

王凤刚(ginobiliwang)

源码分析 分布式事务 greenplum

Greenplum内核源码分析-分布式事务(三)

王凤刚(ginobiliwang)

源码分析 分布式事务 greenplum

谁编写了区块链的规则?

CECBC

尚硅谷喜获央广网2021年度公信力教育品牌

@零度

强强联袂!腾讯云TDSQL与国双战略签约,锚定国产数据库巨大市场

腾讯云数据库

tdsql 国产数据库

TDSQL PostgreSQL如何快速定位阻塞SQL

腾讯云数据库

tdsql 国产数据库

java开发之SSM开发框架的快速理解

@零度

ssm JAVA开发

在线JSON转Schema工具

入门小站

工具

MongoDB基本介绍与安装(1)

Tom弹架构

Java mongodb

从人工到智能!百度AI开发者大会分论坛,探寻国球乒乓背后的AI之路

百度大脑

人工智能

常用的echo和cat,这次让我折在了特殊字符丢失问题上

华为云开发者社区

Linux cat echo 特殊字符 定向

旺链科技团建图鉴 | 认真工作,肆意生活~

旺链科技

区块链 企业文化 团建

【签约计划第二季】百位签约创作者名单公布

InfoQ写作社区官方

签约计划第二季 热门活动

链计算、新基建:区块链助力数字经济新生态

CECBC

Dubbo为什么要用Go重写?

捉虫大师

Go dubbo

取代Maven?maven-mvnd持续霸榜 GitHub Trending,性能提升300%

沉默王二

maven

MongoDB按需物化视图介绍

MongoDB中文社区

mongodb

Reladomo:自备全套功能的企业级开源Java ORM(一)_Java_Mohammad Rezaei_InfoQ精选文章