Loading…

BitBadger.Npgsql.Documents
Setting Up

Namespaces

The examples on this and subsequent pages assume these statements are used. Additionally, F# examples that use let!, do!, etc. are assumed to be within the context of a task or backgroundTask CE.

Configuring a Data Source

ADO.NET in .NET 7 introduced the DbDataSource type, designed to be a singleton which can execute commands, hand out connections, manage a connection pool, etc. Npgsql introduced NpgsqlDataSource as its implementation of this type, and the libraries use this as the basis for its connections. Npgsql also provides NpgsqlDataSourceBuilder type; its static Create method creates a data source from a connection string, and its other methods allow for configuring several other options before creating the data source.

NOTE: Although NpgsqlDataSource implements IDisposable, it should not be created using using (C#) or use (F#), as this will cause it to be disposed when the initialization logic goes out of scope.

Once the data source has been created, telling the library about it is a one-liner:

// C#
using BitBadger.Npgsql.Documents;
using Npgsql;

// ...
    var dataSource = NpgsqlDataSourceBuilder.Create("[conn-str]").Build();
    Configuration.UseDataSource(dataSource);
// ...
// F#
open BitBadger.Npgsql.FSharp.Documents
open Npgsql

// ...
    let dataSource = NpgsqlDataSourceBuilder.Create("[conn-str]").Build ()
    Configuration.useDataSource dataSource
// ...

You may choose to register the data source as a singleton in your dependency injection container of choice, but you can also access this instance via Configuration.DataSource() (C#) or Configuration.dataSource () (F#).

Configuring the ID Field Name

A common .NET pattern when naming unique identifiers for entities / documents / etc. is the name Id. By default, this library assumes that this field is the identifier for your documents. If your code follows this pattern, you will be happy with the default behavior. If you use a different property, or implement a custom serializer to modify the JSON representation of your documents' IDs, though, you will need to configure that field name before you begin calling other functions or methods. A great spot for this is just after you configure the data source (above). If you have decided that the field “Name” is the unique identifier for your documents, your setup would look something like…

// C#
Configuration.UseIdField("Name");
// F#
Configuration.useIdField "Name"

Setting this will make EnsureTable create the unique index on that field when it creates a table, and will make all the ById functions and methods look for data ->> 'Name' instead of data ->> 'Id'. JSON is case-sensitive, so if the JSON is camel-cased, this should be configured to be id instead of Id (or name to follow the example above).

Ensuring Tables and Indexes Exist

As a standard relational database, PostgreSQL stores data in tables, and utilizes indexes to retrieve that data. Each application will need to determine the tables and indexes it expects.

As a full relational database, we can also utilize schemas to organize/isolate our data. Everywhere this library expects a table name, a qualified table name (schema.table) may be provided. Creating these schemas is outside the scope of functionality provided by this library; CREATE SCHEMA IF NOT EXISTS is your friend here.

An example will probably help the concepts make sense. For the purposes of our example, let's consider a hotel chain; they have several hotels, and each hotel has several rooms. While each hotel could have its rooms as part of a Hotel document, there would likely be a lot of contention when concurrent updates for rooms, so we will put rooms in their own table. The hotel will store attributes like name, address, etc.; while each room will have the hotel's ID (named Id), along with things like room number, floor, and a list of date ranges where the room is not available. (This could be for customer reservation, maintenance, etc.)

PostgreSQL provides two options for indexing JSONB fields; one indexes all fields in a way where all JSON operators work well against it, while the other excludes a few operators in exchange for faster performance. This library refers to the regular GIN index as Full, while the jsonb_path_ops variant is Optimized. For our examples, we have decided that the hotels need a full index, while the rooms need an optimized index.

This library's table and index creation both use IF NOT EXISTS, so they are safe to use in application startup. If the table/index exists, nothing happens; however, if they do not, they are created. To implement our example requirements, we would use the following code:

// C#
    await Definition.EnsureTable("hotel");
    await Definition.EnsureIndex("hotel", DocumentIndex.Full);
    await Definition.EnsureTable("room");
    await Definition.EnsureIndex("room", DocumentIndex.Optimized);
// F#
    do! Definition.ensureTable "hotel"
    do! Definition.ensureIndex "hotel" Full
    do! Definition.ensureTable "room"
    do! Definition.ensureIndex "room" Optimized

Now that we have tables, let's use them!