
我们是一家大型公司部门,使用Java SE / MS SQL / db4o开发重要的系统。 几年来,该项目从原型转向工业运行,而db4o变成了计算制动器,我想从db4o转向现代noSQL技术。 反复试验导致的错误与最初的计划相去甚远-可以拒绝db4o,但要以折衷为代价。 在猫的思考和实现细节。
db4o技术死了吗?
在Habré上,可能找不到太多有关db4o的出版物 。 在Stackoverflow上,某种剩余的活动就像是对一个旧问题的新评论或一个新的未回答的问题 。 Wiki通常认为当前的稳定版本是2011年。
这形成了一个总体印象:技术无关紧要。 甚至有官方确认 :Actian决定不再积极为新客户开发和推广商业db4o产品。
db4o是如何计算的
面向对象数据库简介一文讨论了db4o的主要功能-完全没有数据方案。 您可以创建任何对象
User user1 = new User("Vasya", "123456", 25);
然后将其写入数据库文件
db.Store(user1)
然后可以使用Query.execute()方法以保存对象的形式检索记录的对象。
在项目开始时,这使得可以快速确保显示所有提交的数据的审计跟踪,而不必担心关系表的结构。 这帮助该项目得以幸存。 然后,沙箱中的资源很少,今天的计算结束后,明天的数据立即开始加载到MS SQL中。 一切都在不断变化-请找出在晚上自动提供的服务。 并且可以访问db4o文件
在调试中,提取所需日期的快照并回答“我们已提交所有数据,但您未订购任何东西”的问题。
随着时间的流逝,生存问题消失了,项目开始了,用户需求的工作也发生了变化。 在调试中打开db4o文件并解析一个难题,开发人员总是很忙。 取而代之的是,有一大堆分析师对订单逻辑进行了描述,并且只能使用用户可见的数据部分。 不久,db4o开始仅用于显示计算历史记录。 就像Pareto一样,功能的一小部分提供了主要的负载。
在战斗操作中,历史记录文件每天需要约35 GB,而卸载大约需要一个小时。 该文件本身压缩得很好(1:10),但是由于某些原因com.db4o.ObjectContainer库无法执行压缩。 在CentOS的北部,com.db4o.query.Query库只对一个流写入/读取文件。 速度是瓶颈。
设备示意图
系统的信息模型是对象A,B,C和D的层次结构,该层次结构不是树;操作需要链接C1-> B1。
ROOT || | ==>A1 | || | | ==> B1 <------ | | || | | | | ======> C1 | | | | | | | ===> C1.$D | | =======> C2 | | | | ==> B2 ==> C2.$D | | ===>A2 =======> C3 | | ==> B3 ===> C3.$D | ======> C4 | ===> C4.$D
用户通过com.sun.net.httpserver.HttpsServer提供的用户界面(GUI)与服务器进行交互,客户端和服务器交换XML文档。 在第一个显示屏上,服务器为用户级别分配一个标识符,此标识符不会进一步更改。 如果用户需要某种级别的历史记录,则GUI会向服务器发送XML封装的标识符。 服务器确定用于搜索数据库的键的值,在db4o文件中扫描所需的日期,并在内存中检索所请求的对象以及它所引用的所有对象。 构建提取级别的XML表示,并将其返回给客户端。
扫描文件时,缺省情况下,db40会将所有子对象读取到一定深度,并与所需对象一起提取相当大的层次结构。 通过使用conf.common()。ObjectClass(Foo.class).maximumActivationDepth(1)设置不必要的Foo类的最小激活深度,可以减少读取时间。
使用匿名类将导致隐式引用该封闭类$ 0 。 db4o正确(但缓慢)处理并还原此类链接。
0.想法
因此,在支持或管理db4o时,管理员的表情很奇怪。 数据提取速度慢,技术不是很活跃。 任务:代替当前的db4o,应用当前的NoSQL技术。 一双Spring Data + MongoDB引起了我的注意。
1.正面方法
我的第一个想法是使用org.springframework.data.mongodb.core.MongoOperations和save()方法,因为它看起来像com.db4o.ObjectContainer.db.Store(user1)。 MongoDB文档说文档存储在集合中,将必要的系统对象表示为相应集合的文档是合乎逻辑的。 也有@DBRef批注 ,使您可以按照3NF的精神总体上实现文档之间的关系。 走吧
1.1。 正在卸载。 关键参考类型
该系统由很久以前设计的POJO类组成,没有考虑所有这些新技术。 使用Map <POJO,POJO>类型的字段,存在使用它们的分支逻辑。 我保存此字段,但出现错误
org.springframework.data.mapping.MappingException: Cannot use a complex object as a key value.
在这种情况下,仅发现了2011年的对应文件 ,其中有人提议开发非标准的MappingMongoConverter。 到目前为止,我注意到了问题字段@瞬态。 原来是保存,研究结果。
保存发生在集合中,其名称与保存的类的名称一致。 我尚未使用@DBRef批注,因此只有一个集合,JSON文档很大且分支。 我注意到,当您保存对象时,MongoOperations会遍历所有(包括继承的)非空链接,并将它们写为附件。
1.2。 正在卸载。 命名字段或数组?
该系统模型使类C可以多次包含对同一类D的引用。 在单独的defaultMode字段以及ArrayList中的其他链接中,类似以下内容
public class C { private D defaultMode; private List<D> listOfD = new ArrayList<D>(); public class D { .. } public C(){ this.defaultMode = new D(); listOfD.add(defaultMode); } }
卸载后,JSON文档将具有两个副本:名为defaultMode的附加文档和文档数组的未命名元素。 在第一种情况下,可以通过名称访问文档,在第二种情况下-通过带有索引的数组名称可以访问。 在这两种情况下,您都可以搜索MongoDB集合。 仅使用Spring Data和MongoDB,我得出的结论是,如果谨慎使用,可以使用ArrayList。 我没有注意到使用数组的任何限制。 功能随后出现在MongoDB Connector for BI级别。
1.3。 资料下载 构造函数参数
我正在尝试使用MongoOperations.findOne()方法读取保存的文档。 从数据库加载对象A引发异常
"No property name found on entity class A to bind constructor parameter to!"
事实证明,该类具有corpName字段,构造函数具有String name参数,并且this.corpName = name是在构造函数主体中分配的。 MongoOperations要求类中的字段名称与构造函数参数的名称匹配。 如果有多个构造函数,则需要使用@PersistenceConstructor注释选择一个。 我将字段名称和参数对应起来。
1.4。 资料下载 $ $和$ 0
内部嵌套的类D封装了类C的默认行为,与类C分开没有任何意义。 为C的每个实例创建一个D实例,反之亦然-对于D的每个实例,都有一个C实例生成它,类D仍然具有实现替代行为的后代,并且可以存储在listOfD中。 D的后代类的构造函数要求存在一个已经存在的对象C。
除了嵌套内部类之外,系统还使用匿名内部类。 如您所知 ,它们都包含对封闭类实例的隐式引用。 也就是说,作为CD对象的每个实例的一部分,编译器创建一个指向$ 0的链接,该链接指向父对象C。
我再次尝试从集合中读取保存的文档并得到异常
"No property this$0 found on entity class $D to bind constructor parameter to!"
我记得,类D的方法使用具有may和main的引用C.this.fieldOfClassC,并且类D的后代要求构造函数以C作为参数实例化。 也就是说,我需要提供在MongoOperations中创建对象的特定顺序,以便可以在D构造函数中指定父对象C。再次,非标准MappingMongoConverter?
也许不使用匿名类并使内部类正常? 改进,或者甚至改进已经实现的系统的体系结构,是一个了不起的任务...
2.从3NF / @ DBRef开始
另一方面,我尝试将每个类保存在我的收藏集中,并本着3NF的精神在它们之间建立联系。
2.1。 正在卸载。 @DBRef很漂亮
类C包含对D的多个引用。如果将defaultMode和ArrayList链接标记为@DBRef,则文档的大小将减小,而不是庞大的附加文档,而是会有整洁的链接。 在集合C字段的json文档中出现
"defaultMode" : DBRef("D", ObjectId("5c496eed2c9c212614bb8176"))
在MongoDB数据库中,将自动创建一个集合D,并在其中带有字段的文档
"_id" : ObjectId("5c496eed2c9c212614bb8176")
一切都简单而美丽。
2.2。 资料下载 D类构造函数
使用链接时,C对象知道默认D对象仅创建一次。 如果您需要绕过除默认对象之外的所有D对象,只需比较链接即可:
private D defaultMode; private ArrayList<D> listOfD; for (D currentD: listOfD){ if (currentD == defaultMode) continue; doSomething(currentD); }
我调用findOne(),研究类C。事实证明,MongoOperations读取一个json文档并为遇到的每个@DBRef注释调用D构造函数,每次创建一个新对象。 我得到一个奇怪的构造-在defaultMode字段和listOfD数组中对D的两个不同引用,其中链接应该相同。
向社区学习:“我认为与mongodb合作时应避免使用Dbref。” 官方文档中的另一个考虑也与此类似:将规范化数据模型存储在单个文档中的非规范化数据模型将是解析DBRef的最佳选择,您的应用程序必须执行其他查询才能返回引用的文档。
提到的文档页面一开始就说:“对于MongoDB中的许多用例,将相关数据存储在单个文档中的非规范化数据模型将是最佳的。” 是为我写的吗?
专注于设计人员意味着您不需要像关系DBMS中那样思考。 选择是:
- 如果指定@DBRef:
- 每个注解的构造函数将被调用,并且将创建几个相同的对象;
- MongoOperations将查找并读取所有相关集合中的所有文档。 将通过ObjectId请求建立索引,然后从(大型)数据库的许多集合中读取数据;
- 如果未指定,则“重复”的json将与相同数据的重复一起保存。
我为自己指出:您不能依赖@DBRef,而是使用ObjectId类型的字段来手动填充它。 在这种情况下,
"defaultMode" : DBRef("D", ObjectId("5c496eed2c9c212614bb8176"))
json文件将包含
"defaultMode" : ObjectId("5c496eed2c9c212614bb8176")
将不会自动加载-MongoOperations不知道在哪个集合中搜索文档。 该文档将需要在单独的(延迟)请求中加载,以指示集合和ObjectId。 单个查询应快速返回结果,此外,ObjectId将为每个集合创建一个自动索引。
2.3。 那现在呢?
小计。 无法在MongoDB上快速轻松地实现db4o功能:
- 尚不清楚如何使用自定义POJO作为键-值列表键;
- 目前尚不清楚如何设置在MappingMongoConverter中创建对象的顺序。
- 目前尚不清楚是否上传没有DBRef的“非规范化”文档,是否有必要提出自己的延迟初始化机制。
您可以添加延迟加载。 您可以尝试做MappingMongoConverter。 您可以修改现有的构造函数/字段/列表。 但是,业务逻辑有很多年的层次划分-并非微不足道的变更和从未被测试的风险。
妥协解决方案:创建一种新的机制来保存要解决的问题的数据,同时保留与GUI交互的机制。
3.第三次尝试,前两次的体验
帕累托建议,以用户速度解决问题将意味着整个任务的成功。 任务是这样的:您需要学习如何在没有db4o的情况下快速保存和恢复用户演示数据。
这将失去在调试中检查已保存对象的能力。 一方面,这很不好。 另一方面,此类任务很少发生,并且在git中所有战斗交付都被标记。 为了容错,每次卸载之前,系统都会将计算序列化到文件中。 如果需要在调试中检查对象,则可以进行序列化,克隆相应的系统程序集并恢复计算。
3.1。 自定义演示数据
要构建用户级别的演示文稿,系统具有特殊的Viewer类。 Viewer.getXML()方法接收一个级别作为输入,从中提取必要的数字和字符串值,并生成XML。
如果用户要求显示今天的计算级别,则可以在RAM中找到该级别。 为了显示过去的计算结果,com.db4o.query.Query.execute()方法将在文件中找到该级别。 文件中的级别几乎与刚刚创建的级别没有什么不同,Viewer会在不注意替换的情况下构建演示文稿。
为了解决我的问题,我需要在计算级别与其表示之间使用一个中介-表示框架(Frame),它将存储数据并建立在可用XML数据的基础上。 每当生成一个框架并且该框架将生成XML时,用于构建演示文稿的操作链就会变得更长。
: < > -> Viewer.getXML() : < > -> Viewer.getFrame() -> Frame.getXML()
保存故事时,您将需要构建所有级别的框架并写入数据库。
3.2。 卸货
任务相对简单,并且没有问题。 重复XML表示的结构,该框架以元素层次结构的形式接收了递归设备,该元素具有字段String,Integer和Double。 框架从其所有元素请求getXML(),将其收集到单个文档中,然后返回。 MongoOperations在框架的递归特性方面做得很好,并且在框架发展过程中没有提出新的问题。
最后,一切都开始了! 默认情况下,WiredTiger引擎压缩 MongoDB文档集合;在文件系统上,每天的卸载量约为3.5 GB。 比db4o减少十倍还不错。
最初,卸载的安排很简单-每个级别树的递归遍历MongoOperations.save()。 这样的卸载花费了5.5个小时,尽管构建演示文稿仅涉及读取对象,但这样做仍然很费力。 我添加了多线程:递归地遍历级别树,将所有可用级别拆分为一定大小的数据包,根据数据包的数量创建Callable.call()实现,将每个数据包传输到我们自己的包中,并通过ExecutorService.invokeAll()完成所有操作。
MongoOperations再次没有提出任何问题,并且在多线程模式下做得很好。 根据经验选择包装的尺寸,以达到最佳的卸货速度。 结果是15分钟,一包1000个级别。
3.3。 Mongo BI连接器,或人们如何使用它
MongoDB 查询语言既强大又强大,我不可避免地获得了使用它的经验,并且到达了这个地方。 该控制台支持 JavaScript,您可以编写美观而强大的设计。 这是一方面。 另一方面,我可以提出请求,让一半的分析人员绞尽脑汁
db.users.find( { numbers: { $in: [ 390, 754, 454 ] } } );
而不是通常的
SELECT * FROM users WHERE numbers IN (390, 754, 454)
适用于BI的MongoDB连接器可助您一臂之力,通过它您可以以表格形式显示收集文档。 MongoDB数据库称为基于文档的数据库,它不知道如何以表格形式显示字段/文档的层次结构。 为了使连接器正常工作,有必要在单独的.drdl文件中描述future表的结构,该文件的格式与yaml非常相似。 在文件中,您必须指定输出处的关系表字段与输入处的JSON文档字段路径之间的对应关系。
3.4。 使用数组的特征
上面说过,对于MongoDB本身,数组和字段之间没有特殊区别。 从连接器的角度来看,数组与命名字段有很大的不同。 我什至不得不重构完成的Frame类。 仅当有必要将部分信息放入链接表中时,才应使用文档数组。
如果JSON文档是命名字段的层次结构,则可以通过指定文档根目录到句点的路径来访问任何字段,例如xy。如果在DRDL文件中指定了对应关系xy => fieldXY,则输出表将具有与集合中的文档一样多的行。在入口处。 如果在某些文档中没有xy字段,则表的相应行中将为NULL。
假设我们有一个名为Frames的MongoDB数据库,该数据库中有一个集合A,MongoOperations已向该集合编写了两个类A的实例。
{ "_id": ObjectId("5cdd51e2394faf88a01bd456"), "x": { "y": "xy string value 1"}, "days": [{ "k": "0", "v": 0.0 }, { "k": "1", "v": 0.1 }], "_class": "A" }
第二个(ObjectId的最后一位不同):
{ "_id": ObjectId("5cdd51e2394faf88a01bd457"), "x": { "y": "xy string value 2"}, "days": [{ "k": "0", "v": 0.3 }, { "k": "1", "v": 0.4 }], "_class": "A" }
BI连接器无法通过索引访问数组的元素,例如,根本不可能将数组中的days [1] .v字段提取到表中。 而是,连接器可以使用$ unwind运算符将days数组的每个元素表示为单独表中的一行。 该单独的表将通过行标识符与原始的一对多关系相关联。 在我们的示例中,表tableA为收集文档定义,而tableA_days为days数组的文档定义。 .drdl文件如下所示:
schema: - db: Frames tables: - table: tableA collection: A pipeline: [] columns: - Name: _id MongoType: bson.ObjectId SqlName: _id SqlType: objectid - Name: xy MongoType: string SqlName: fieldXY SqlType: varchar - table: tableA_days collection: A pipeline: - $unwind: path: $days columns: - Name: _id # MongoType: bson.ObjectId SqlName: tableA_id SqlType: objectid - Name: days.k MongoType: string SqlName: tableA_dayNo SqlType: varchar - Name: days.v MongoType: string SqlName: tableA_dayVal SqlType: varchar
表的内容将是:table tableA
和表tableA_days
合计
无法以原始公式来实现该任务;您不能仅将db4o替换为MongoDB即可。 MongoOperations无法自动还原任何对象,例如db4o。 您可能可以执行此操作,但是人工成本将无法与调用db4o库的store / query方法相提并论。
审核跟踪。 Db4o在项目开始时是一个非常有用的工具。 您可以简单地编写对象,然后将其还原,与此同时无需担心和使用表。 所有这些都有一个重要的警告:如果您需要更改类的层次结构(在A和B之间添加类E),那么所有以前存储的信息将变得不可读。 但是对于启动项目来说,这不是很重要,只要不存在大量的旧文件累积即可。
当MongoOperations有足够的经验时,编写上载不会引起问题。 为该框架编写新代码比重做旧代码要容易得多,后者也已投入生产。