BitBadger.Npgsql.Documents
Setting Up
Namespaces
- C#
using BitBadger.Npgsql.Documents;
will make the various static classes and enumerations visible. - F#
open BitBadger.Npgsql.FSharp.Documents
will make the modules and DUs visible.
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 atask
orbackgroundTask
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
implementsIDisposable
, it should not be created usingusing
(C#) oruse
(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!