lmdb-safe/README.md

336 lines
12 KiB
Markdown
Raw Permalink Normal View History

2018-12-05 15:46:05 +01:00
# lmdb-safe
A safe modern & performant C++ wrapper of LMDB.
Requires C++17, or C++11 + Boost.
2018-12-15 01:54:29 +01:00
2018-12-08 14:08:26 +01:00
[LMDB](http://www.lmdb.tech/doc/index.html) is an outrageously fast
key/value store with semantics that make it highly interesting for many
2018-12-08 20:57:39 +01:00
applications. Of specific note, besides speed, is the full support for
transactions and good read/write concurrency. LMDB is also famed for its
robustness.. **when used correctly**.
2018-12-08 14:08:26 +01:00
The design of LMDB is elegant and simple, which aids both the performance
2018-12-08 20:57:39 +01:00
and stability. The downside of this elegant design is a [nontrivial set of
rules](http://www.lmdb.tech/doc/starting.html)
that [need to be followed](http://www.lmdb.tech/doc/group__mdb.html) to not break things. In other words, LMDB delivers
great things but only if you use it exactly right. This is by [conscious
design](https://twitter.com/hyc_symas/status/1056168832606392320).
2018-12-08 14:08:26 +01:00
Among the things to keep in mind when using LMDB natively:
* Never open a database file more than once anywhere in your process
* Never open more than one transaction within a thread
2018-12-08 20:57:39 +01:00
* .. unless they are all read-only and have MDB_NOTLS set
2018-12-08 14:08:26 +01:00
* When opening a named database, no other threads may do that at the same time
* Cursors within RO transactions need freeing, but cursors within RW
transactions must not be freed.
* A new transaction may indicate the database has grown, and you need to
restart the transaction then.
2018-12-08 14:08:26 +01:00
Breaking these rules may cause no immediate errors, but can lead to silent
2018-12-08 20:57:39 +01:00
data corruption, missing updates, or random crashes. Again, this is not an
actual bug in LMDB, it means that LMDB expects you to use it according to
its exact rules. And who are we to disagree?
2018-12-08 14:08:26 +01:00
2018-12-08 20:57:39 +01:00
The `lmdb-safe` library aims to deliver the full LMDB performance while
2018-12-08 14:08:26 +01:00
programmatically making sure the LMDB semantics are adhered to, with very
limited overhead.
Most common LMDB functionality is wrapped within this library but the native
MDB handles are all available should you want to use functionality we did
not (yet) cater for.
2018-12-15 01:54:29 +01:00
In addition, on top of `lmdb-safe`, a type-safe ["Object Relational
Mapping"](https://en.wikipedia.org/wiki/Object-relational_mapping) interface
is also available. This auto-generates indexes and allows for the insertion,
deletion and iteration of objects.
2018-12-08 20:57:39 +01:00
# Status
2018-12-10 17:42:25 +01:00
Fresh. If using this tiny library, be aware things might change
2018-12-15 01:54:29 +01:00
rapidly. To use, add `lmdb-safe.cc` and `lmdb-safe.hh` to your project. In
addition, add `lmdb-typed.hh` to use the ORM.
2018-12-08 20:57:39 +01:00
# Philosophy
This library tries to not restrict your use of LMDB, nor make it slower,
except on operations that should be rare. The native LMDB handles
(Environment, DBI, Transactions & Cursors) are all available for your direct
use if need be.
When using `lmdb-safe`, errors "that should never happen" are turned into
exceptions. An error that merely indicates that a key can not be found is
passed on as a regular LMDB error code.
2018-12-08 14:08:26 +01:00
# Example
The following example has no overhead compared to native LMDB, but already
2018-12-08 20:57:39 +01:00
exhibits several ways in which lmdb-safe automates LMDB constraints:
2018-12-08 14:08:26 +01:00
```
auto env = getMDBEnv("./database", 0, 0600);
auto dbi = env->openDB("example", MDB_CREATE);
auto txn = env->getRWTransaction();
```
The first line requests an LMDB environment for a database hosted in
2018-12-08 20:57:39 +01:00
`./database`. **Within LMDB, it is not allowed to open a database file more
than once**, not even from other threads, not even when using a different
LMDB handle. `getMDBEnv` keeps a registry of LMDB environments, keyed to
the exact inode & flags. If another part of your process requests access to
the same inode, it will get the same environment. `MDBEnv` is threadsafe.
2018-12-08 14:08:26 +01:00
On the second line, a database is opened within our environment. The
semantics of opening or creating a database within LMDB are tricky. With
some loss of generality, `MDBEnv::openDB` will create a transaction for you
to open the database, and close it too. Most of the time this is what you
want. It is also possible to open a database within a transaction manually.
The third line opens a read/write transaction using the Resource Acquisition
Is Initialization (RAII) technique. If `txn` goes out of scope, the
transaction is aborted automatically. To commit or abort, use `commit()` or
`abort()`, after which going out of scope has no further effect.
```
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
txn->put(dbi, "lmdb", "great");
2018-12-08 14:08:26 +01:00
string_view data;
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
if(!txn->get(dbi, "lmdb", data)) {
2018-12-08 14:08:26 +01:00
cout<< "Within RW transaction, found that lmdb = " << data <<endl;
}
else
cout<<"Found nothing" << endl;
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
txn->commit();
2018-12-08 14:08:26 +01:00
```
LMDB is so fast because it does not copy data unless it really needs to.
Memory bandwidth is a huge determinant of performance on modern CPUs. This
2022-01-18 22:37:17 +01:00
wrapper agrees, and using modern C++ makes it possible to seamlessly use
2018-12-08 14:08:26 +01:00
'views' on data without copying them. Using these techniques, the call to
`txn.put()` sets the "lmdb" string to "great", without making additional
copies.
We employ the same technique to request the value of "lmdb", which is made
available to us as a read-only view, straight onto the memory mapped data on
disk.
In the final line, we commit the transaction, after which it also becomes
available for other threads and processes.
2018-12-08 20:57:39 +01:00
A slightly expanded version of this code can be found in
[basic-example.cc](basic-example.cc).
2018-12-10 17:42:25 +01:00
# Input and output of values
The basic data unit of LMDB is `MDB_val` which describes a slab of memory.
Within LMDB, `MDB_val` is used for both input and output. For safety
purposes, in this library we split this up into `MDBInValue` and
`MDBOutValue`. Once split, we can add some very convenient semantics to these
classes.
For example, to store `double` values for 64 bit IDs:
```
auto txn = env->getRWTransaction();
uint64_t id=12345678901;
double score=3.14159;
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
txn->put(dbi, id, score);
txn->commit();
2018-12-10 17:42:25 +01:00
```
Behind the scenes, the `id` and `score` values are wrapped by `MDBInVal`
2022-01-18 22:37:17 +01:00
which converts these values into byte strings. To retrieve these values
works similarly:
2018-12-10 17:42:25 +01:00
```
auto txn = env->getRWTransaction();
uint64_t id=12345678901;
MDBOutValue val;
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
txn->get(dbi, id, val);
2018-12-10 17:42:25 +01:00
cout << "Score: " << val.get<double>() << "\n";
```
Note that on retrieval, we have to specify the type of the value stored.
This allows the conversion back from a byte string into the native type.
`MDBOutValue` also tests if the length of the data matches the type.
## Details
The automatic conversion to and from the `MDBVal`s is implemented strictly
for:
* Integer and floating point types
* std::string
* std::string_view
However, if you explicitly ask for it, it is also possible to serialize
`struct`s:
```
struct Coordinate
{
double x,y;
};
C c{12.0, 13.0};
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
txn->put(dbi, MDBInVal::fromStruct(c), 12.0);
2018-12-10 17:42:25 +01:00
MDBOutVal res;
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
txn->get(dbi, MDBInVal::fromStruct(c), res);
2018-12-10 17:42:25 +01:00
auto c1 = res.get_struct<Coordinate>();
```
2018-12-08 20:57:39 +01:00
# Cursors, transactions
This example shows how to use cursors and how to mix `lmdb-safe` with direct
calls to mdb.
```
auto env = getMDBEnv("./database", 0, 0600);
auto dbi = env->openDB("huge", MDB_CREATE);
auto txn = env->getRWTransaction();
unsigned int limit=20000000;
```
This is the usual opening sequence.
```
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
auto cursor=txn->getCursor(dbi);
2018-12-10 17:42:25 +01:00
MDBOutVal key, data;
2018-12-08 20:57:39 +01:00
int count=0;
cout<<"Counting records.. "; cout.flush();
while(!cursor.get(key, data, count ? MDB_NEXT : MDB_FIRST)) {
count++;
}
cout<<"Have "<<count<<"!"<<endl;
```
This describes how we generate a cursor for the `huge` database and iterate
over it to count the number of keys in there. We pass two LMDB native
`MDB_val` structs to the cursor `get` function. These do not get copies of
all the millions of potential keys in the `huge` database - they only
contain pointers to that data. Because of this, we can count 20 million
records in under a second (!).
```
cout<<"Clearing records.. "; cout.flush();
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
mdb_drop(*txn, dbi, 0); // clear records
2018-12-08 20:57:39 +01:00
cout<<"Done!"<<endl;
```
Here we drop al keys from the database, which too happens nearly
instantaneously. Note that we pass our `txn` (which is a class) to the
native `mdb_drop` function which we did not wrap. This is possible because
`txn` converts to an `MDB_env*` if needed.
```
cout << "Adding "<<limit<<" values .. "; cout.flush();
for(unsigned int n = 0 ; n < limit; ++n) {
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
txn->put(dbi, n, n);
2018-12-08 20:57:39 +01:00
}
cout <<"Done!"<<endl;
cout <<"Calling commit.. "; cout.flush();
Hide MDB*Transaction behind a unique_ptr front This is to prevent the issue with Object Slicing. With the previous solution (where MDB*Transaction are normal objects), consider the following code: MDBRWTransaction txn = env.getRWTransaction(); //! Invalid: We explicitly break this move because it would be //! unsafe: // MDBROTransaction ro_txn(std::move(txn)); //! Valid, RW inherits from RO now, so we can bind an RO //! reference to an RW transaction. MDBROTransaction &ro_txn = txn; //! Dangerous!! MDBROTransaction ro_txn2(std::move(ro_txn)); The last move there breaks the semantics of the RW transaction which is bound to the reference ro_txn. It looses its RW cursors, which remain partly inside the txn instance. All kinds of weird and bad things can happen here. For instance, the ro_txn2 would go out of scope before the txn, calling the destructor MDBROTransaction destructor (which defaults to commit instead of abort!) and only freeing parts of the cursors. Only then the MDBRWTransaction destructor is called, which will free the cursors which belong to the RW transaction which has already been committed. The only safe way to prevent Object Slicing in this scenario I could come up with is to disallow moves of the objects altogether and instead use unique_ptr as front for them. This also removes an additional dynamic allocation per RW transaction (for the cursor vector), since the address of that vector is now constant over the lifetime of the transaction without indirection.
2019-10-26 11:42:38 +02:00
txn->commit();
2018-12-08 20:57:39 +01:00
cout<<"Done!"<<endl;
```
2018-12-10 17:42:25 +01:00
Here we add 20 million value & then commit the `mdb_drop` and the 20 million
puts. All this happened in less than 20 seconds.
2018-12-08 20:57:39 +01:00
Had we created our database with the `MDB_INTEGERKEY` option and added the
`MDB_APPEND` flag to `txn.put`, the whole process would have taken around 5
2018-12-15 01:54:29 +01:00
seconds.
# lmdb-typed
The `lmdb-safe` interface may be safe in one sense, but it is still a
key-value store, allowing the user to store any key and any value.
Frequently we have specific needs: to store objects and find them using
different keys. Doing so manually is cumbersome and error-prone, as all
indexes (for rapid retrieval) need to be carefully maintained by hand.
Inspired by Boost MultiIndex, `lmdb-typed` builds on `lmdb-safe` to create,
populate and use indexes for rapidly retrieving objects. As an example,
let's say we want to store the following struct:
```
struct DNSResourceRecord
{
string qname; // index
uint16_t qtype{0};
uint32_t domain_id{0}; // index
string content;
uint32_t ttl{0};
string ordername; // index
bool auth{true};
};
```
And we want to do so based on the `qname`, `domain_id` or `ordername`
2022-01-18 22:35:23 +01:00
fields. First, we have to make sure DNSResourceRecord can be (de)serialized
to/from a string. For that you need to implement `serToString(const T& t)` and
`serFromString(const string_view& str, T& ret)` like it is shown in
`lmdb-reflective.hh` which uses
[Reflective RapidJSON](https://github.com/Martchus/reflective-rapidjson)'s
binary (de)serializer to ease that task. You can also use Boost.Serialization.
It might also be helpful to utilize Boost.Hana, e.g. in combination with the
previously mentioned libraries.
2018-12-15 01:54:29 +01:00
Next up, we need to define our "Object Relational Mapper":
```
TypedDBI<DNSResourceRecord,
index_on<DNSResourceRecord, string, &DNSResourceRecord::qname>,
index_on<DNSResourceRecord, uint32_t, &DNSResourceRecord::domain_id>,
index_on<DNSResourceRecord, string, &DNSResourceRecord::ordername>
> tdbi(getMDBEnv("./typed.lmdb", MDB_NOSUBDIR, 0600), "records");
```
This defines that we create a database called `records` in the file
`./typed.lmdb`. We also state that this database stores `DNSResourceRecord`
objects, and that we want three indexes. Note that this syntax is reasonable
similar to that used by Boost::MultiIndex.
Next up, we can insert some objects:
```
auto txn = tdbi.getRWTransaction();
DNSResourceRecord rr{"www.powerdns.com", 1, domain_id, "1.2.3.4", 0, "www"};
// populate rr
auto id = txn.insert(rr);
txn.commit();
```
Internally, the opening of `tdbi` above created four databases: `records`,
`records_0`, `records_1` and `records_2`. On insert, a serialized form of
`rr` was stored in the `records` table, with the key containing the
(assigned) id value.
In addition, in `records_1`, the qname was added as key, with the `id` field
as value. And similarly for `domain_id` and `ordername`. So the indexes all
point to the id field, which we can find in the `records` database.
To retrieve, we can use any of the indexes:
```
auto txn = tdbi.getROTransaction();
DNSResourceRecord rr;
txn.get(id, rr);
txn.get<0>("www.powerdns.com", rr);
txn.get<1>(domain_id, rr);
txn.get<2>("www", rr);
```
As long as we inserted only the one `DNSResourceRecord` from above, all four
`get` calls find the same `rr`.
In the more interesting case where we inserted more DNS records, we could
iterate over all items with `domain_id = 4` as follows:
```
for(auto [iter, end] = txn.equal_range<1>(4): iter != end; ++iter) {
2018-12-15 01:54:29 +01:00
cout << iter->qname << "\n";
}
```
To delete an item, use `txn.del(12)`, which will remove the record with id
12 from the main database and also from all the indexes.