logo
back Back to Blog

What is the difference between AceDB and MayronDB?

Author

Mayron

6 min read

Yesterday on a reddit post I talked about MayronObjects, Pkg-MayronDB and Pkg-MayronEvents (all of which are described on mayronui.com/downloads). I was asked the following very good question regarding Pkg-MayronDB and Pkg-MayronEvents:

How do they differ from Ace3-db and Ace3-event? I've been using these two for years to accomplish similar goals to what you have listed. Is there a strong reason yours are worth porting to?

Now, truthfully I have never used Ace3-events and I wrote Pkg-MayronEvents (originally called LibMayronEvents) for personal use when writing MayronUI. I found it useful enough to release it and have extended its functionality to support a wider range of use-cases, but Pkg-MayronDB (originally called LibMayronDB) is a different story. I have used Ace3-db for a long time before I made this package and the reasons I wrote my own was simply because there were things I didn't like with Ace3-db.

I have already wrote a previous blog post on how table inheritance works with Pkg-MayronDB, and this is a huge feature that is unique to Pkg-MayronDB. This feature allows you to define one database table containing default addon settings for a given entity, such as a unit frame or cast bar, and have multiple other tables inherit from it. This avoids the need to repeat yourself and there are several good use-cases for this. You can read all about this on my previous blog post.

Update Functions

One significant thing I disliked about Ace-db was how the updating logic for addons using this library is often mixed tightly into the configuration table. If you take a look at other addons using Ace3-db that have a large number of configuration options for the user, you will see a lot of this:

selfcastrightclick = { -- other settings removed for brevity get = getFunc, set = function(info, value) Bartender4.db.profile.selfcastrightclick = value Bartender4.Bar:ForAll("UpdateSelfCast") end, },

This code comes from Bartender's Git repo. This is a short example but some of these get/set functions can be very large. If changing a setting requires the involvement of multiple modules then you need to import them and check pre-conditional logic to see if certain things need updating. Bartender tries to do something similar to what Pkg-MayronDB solves; it attempts to split this coupling by calling the real update function somewhere else by referencing UpdateSelfCast as a string.

Pkg-MayronDB lets you register update functions (at the time of writing this, this part of the documentation is missing in mayronui.com/p/pkg-mayron-db but I should have that sorted in the next few days). This idea takes things one step further as you don't even need to manually trigger this function. It is registered with the database "Observer" node itself. Each database sub-table is considered an Observer (or a proxy) node. It listens for indexes (trying to access values in its table) as well as new indexes (assigning new values to the table) using Lua meta-tables. So when a new index is detected, Pkg-MayronDB will check if there is an update function associated with that node's "path address" (e.g. "db.profile.module.castbars" represents the path address of the node).

MayronUI uses this feature but it also customises how it works using the manual override parameter to first check if the module associated with that setting is enabled. If it is enabled then I run the function. The Pkg-MayronDB RegisterUpdateFunctions function is actually called later, which looks like this.

By separating out the logic, your configuration table does not need to contain any logic for updating your addon's appearance. It all happens somewhere else and you can customise how such functions get called if the default behavior of Pkg-MayronDB (to call those functions immediately) is not good enough.

Please bare in mind that update functions are completely optional and you don't have to use them.

Intelligent Lookups

Besides the addition of update functions, I also mentioned how it intercepts index and new index requests using Lua meta-tables. The whole database "tree" consists of observer nodes that intelligently clean up unused tables, as well as selecting the correct table to use. When you index a given node, such as db.profile.myModule.height, it will first check the saved variable table, if it returns nil it will check the parent table if the node has one (I mentioned about how the database supports table inheritance in the original post but I also wanted to expand upon this so I added it as a blog post shortly after posting on reddit), if that node does not have a parent or the value found in the parent table is still nil then it checks the defaults table (ace-db has this I think? It's been a while) and finally it checks the parents default table. The code for this is here.

Because it intercepts indexing, it can clean up unused tables, so if you set nil on a value and that was the only value left in the table, then it also recycles the table for a short period of time before deleting it. This also means your saved variables table is kept short. For example, say your saved variables table containging your database looked like this:

MyDB = { global = { module1 = { enabled = true, }, module2 = { theme = { color= "#3399ff" }, }, }, }

And then you ran db.global.module2.theme.color = nil. MayronDB would then prune the saved variables table, transforming it to look like:

MyDB = { global = { module1 = { enabled = true, }, }, }

You can also use db:ParsePathValue(path) to check if a node exists by traversing tables (even if some of the tables in that path don't exist), and db:SetPathValue(path) to create a value in the database and create multiple tables if they don't exist.

So, you can run:

db:SetPathValue("global.module2.aSubTable[".. attributeName .."][5]", 123);

And even though module2 and aSubTable do not exist, this function will create those tables.

Small Quality of Life Improvements

I will finish by highlighting some nice but small additions to Pkg-MayronDB:

  1. You can delete a profile but then restore it before you log out in case it was a mistake using db:RestoreProfile(profile). There have been many times where I have deleted the wrong profile and it is always a frustrating experience, especially if that profile had been highly customised.
  2. db:AppendOnce(...) lets you append values to the saved variables table and register that you have done this so it never happens again until you were to remove the appended key. This is useful if you want to inject settings into the database but you might want to allow the user to set these values to nil but you want to remember that you have already injected them previously so you shouldn't do it again.
  3. You can call GetTrackedTable() on an observer node to get a deep-copy version of the targetted database sub-table. It squashes all of the default values, inherited tables and saved variables table into a brand new table, disconnected from the real saved variables table. You can then make lots of changes and only when you are happy with those changes you can press SaveChanges() on the "tracked table" to apply them to the underlining saved variables table. This skips all the advanced meta-table magic and may improve performance if you need to execute lots of database changes very frequently, such as in an OnUpdate script which runs for each frame (e.g., 60 FPS = 60 OnUpdate script executions). You can also call ResetChanges() to reset the table back to when it was created, before you changed the table, similar to how an SQL transaction works during a rollback.

There are many other features relating to the API, which I recommend reading if you want to learn more.

Author

Written By

Mayron

back Back to Blog

© 2021 MayronUI.com, All rights reserved.