Repositories
The purpose of this guide to serve as a reference for our source generated repositories and our use of the repository pattern in the server codebase

What is the Repository Pattern?

"The simplest approach, especially with an existing system, is to create a new Repository implementation for each business object you need to store to or retrieve from your persistence layer. Further, you should only implement the specific methods you are calling in your application."
Repository Pattern Illustration A
Repository Pattern Illustration B
The underlying persistence layer could be anything, file, memory, remote storage, but in most cases relevant to what we are using repositories for right now in this article pertain to the database (MySQL)

Benefits

    Simplified interaction with the database, instead of manually looking up columns, your IDE will autocomplete the struct fields that you are working with
    Heavily reducing the mental overhead of having to interact with the database at the programmatic level
    It creates an object representation of our tables (persistence layer) with a 1:1 mapping with what is in the source by having row values stored in a struct (Data Transfer Object)
    We reduce manual overhead by having code generation generate our database table representations
    Ease of maintenance; whenever we make schema changes, rerunning the repository updates fields and subsequent methods keeping code changes minimal

Methods Implemented in Base Repository

    Single insertion covered by InsertOne
    Single update covered by UpdateOne
    Single delete covered by DeleteOne
    Single select covered by FindOne
    Bulk selection method via filtered GetWhere(std::string where_filter)
    Bulk deletion method via filtered DeleteWhere(std::string where_filter)
    Bulk insertion methods handled automatically via InsertMany
    Select all covered by All

Code Generation: Repository Generator

The repository generator can be found in the source via the following path
1
./utils/scripts/generators/repository-generator.pl
Copied!
Linux bash
Windows cmd
1
# Command Structure
2
perl ~/code/utils/scripts/generators/repository-generator.pl [server-location] [table_name|all]
3
4
# Generate everything
5
perl ~/code/utils/scripts/generators/repository-generator.pl ~/server/ all
6
7
# Only generate a repository for the account table
8
perl ~/code/utils/scripts/generators/repository-generator.pl ~/server/ account
Copied!
1
# Command Structure
2
# Must be run from the root eqemu source folder
3
# [server-location] is the full path to server's world.exe location
4
E:\EQEmu\src>perl utils/scripts/generators/repository-generator.pl [server-location] [table_name|all]
5
6
# Generate everything
7
E:\EQEmu\src>perl utils/scripts/generators/repository-generator.pl E:\EQEmu\build\bin\Debug\ all
8
9
# Only generate a repository for the account table
10
E:\EQEmu\src>perl utils/scripts/generators/repository-generator.pl E:\EQEmu\build\bin\Debug\ account
11
Copied!
The generator works by examining your database tables found in the connection properties registered in the server config eqemu_config and will generate a struct object that tries to represent the database data types as close as possible

Extending the Base Repository

The base repository is not meant to be touched manually; it is meant to provide generic scaffolding to the underlying persistence layer with generic methods typically needed
To implement your own customary methods you simply add your own methods in the extended repository, for each repository generated it outputs two files, the base repository (immutable) and the extended repository (mutable)
For example when we generate repositories from a single table we get both
./common/repositories/base/base_instance_list_repository.h
and
./common/repositories/instance_list_repository.h
The class structure in the non-base repository looks something like this
1
#include "../database.h"
2
#include "../string_util.h"
3
#include "base/base_instance_list_repository.h"
4
5
class InstanceListRepository: public BaseInstanceListRepository {
6
public:
7
8
/**
9
* This file was auto generated and can be modified and extended upon
10
*
11
* Base repository methods are automatically
12
* generated in the "base" version of this repository. The base repository
13
* is immutable and to be left untouched, while methods in this class
14
* are used as extension methods for more specific persistence-layer
15
* accessors or mutators.
16
*
17
* Base Methods (Subject to be expanded upon in time)
18
*
19
* Note: Not all tables are designed appropriately to fit functionality with all base methods
20
*
21
* InsertOne
22
* UpdateOne
23
* DeleteOne
24
* FindOne
25
* GetWhere(std::string where_filter)
26
* DeleteWhere(std::string where_filter)
27
* InsertMany
28
* All
29
*
30
* Example custom methods in a repository
31
*
32
* InstanceListRepository::GetByZoneAndVersion(int zone_id, int zone_version)
33
* InstanceListRepository::GetWhereNeverExpires()
34
* InstanceListRepository::GetWhereXAndY()
35
* InstanceListRepository::DeleteWhereXAndY()
36
*
37
* Most of the above could be covered by base methods, but if you as a developer
38
* find yourself re-using logic for other parts of the code, its best to just make a
39
* method that can be re-used easily elsewhere especially if it can use a base repository
40
* method and encapsulate filters there
41
*/
42
43
// Custom extended repository methods here
44
45
};
Copied!
Pretty bare right? That's because it should be, this leaves the base repository free to be regenerated later by the repository generator if updates are supplied in the future and allows you to make all of your custom update / delete / find methods in the extended repository without requiring a ton of work to merge or update repositories

Using Repositories in Server Code

Below we have an example implemented using our CLI menu interface to simply test some code
In world_server_command_handler.cpp we've registered a test command for testing repository code
1
/**
2
* Register commands
3
*/
4
function_map["test:test"] = &WorldserverCommandHandler::TestCommand;
5
function_map["test:expansion"] = &WorldserverCommandHandler::ExpansionTestCommand;
6
function_map["test:repository"] = &WorldserverCommandHandler::TestRepository;
Copied!

Base Repository Contents (Truncated)

1
class BaseInstanceListRepository {
2
public:
3
struct InstanceList {
4
int id;
5
int zone;
6
int version;
7
int is_global;
8
int start_time;
9
int duration;
10
int never_expires;
11
};
12
13
static std::string PrimaryKey()
14
{
15
return std::string("id");
16
}
17
18
static std::vector<std::string> Columns()
19
{
20
return {
21
"id",
22
"zone",
23
"version",
24
"is_global",
25
"start_time",
26
"duration",
27
"never_expires",
28
};
29
}
30
...
Copied!

Command Code

1
/**
2
* @param argc
3
* @param argv
4
* @param cmd
5
* @param description
6
*/
7
void TestRepository(int argc, char **argv, argh::parser &cmd, std::string &description)
8
{
9
description = "Test command";
10
11
if (cmd[{"-h", "--help"}]) {
12
return;
13
}
14
15
/**
16
* Insert one
17
*/
18
auto instance_list_entry = InstanceListRepository::NewEntity();
19
20
instance_list_entry.zone = 999;
21
instance_list_entry.version = 1;
22
instance_list_entry.is_global = 1;
23
instance_list_entry.start_time = 0;
24
instance_list_entry.duration = 0;
25
instance_list_entry.never_expires = 1;
26
27
auto instance_list_inserted = InstanceListRepository::InsertOne(instance_list_entry);
28
29
LogInfo("Inserted ID is [{}] zone [{}]", instance_list_inserted.id, instance_list_inserted.zone);
30
31
/**
32
* Find one
33
*/
34
auto found_instance_list = InstanceListRepository::FindOne(instance_list_inserted.id);
35
36
LogInfo("Found ID is [{}] zone [{}]", found_instance_list.id, found_instance_list.zone);
37
38
/**
39
* Update one
40
*/
41
LogInfo("Updating instance id [{}] zone [{}]", found_instance_list.id, found_instance_list.zone);
42
43
int update_instance_list_count = InstanceListRepository::UpdateOne(found_instance_list);
44
45
found_instance_list.zone = 777;
46
47
LogInfo(
48
"Updated instance id [{}] zone [{}] affected [{}]",
49
found_instance_list.id,
50
found_instance_list.zone,
51
update_instance_list_count
52
);
53
54
55
/**
56
* Delete one
57
*/
58
int deleted = InstanceListRepository::DeleteOne(found_instance_list.id);
59
60
LogInfo("Deleting one instance [{}] deleted count [{}]", found_instance_list.id, deleted);
61
62
/**
63
* Insert many
64
*/
65
std::vector<InstanceListRepository::InstanceList> instance_lists;
66
67
auto instance_list_entry_bulk = InstanceListRepository::NewEntity();
68
69
instance_list_entry_bulk.zone = 999;
70
instance_list_entry_bulk.version = 1;
71
instance_list_entry_bulk.is_global = 1;
72
instance_list_entry_bulk.start_time = 0;
73
instance_list_entry_bulk.duration = 0;
74
instance_list_entry_bulk.never_expires = 1;
75
76
for (int i = 0; i < 10; i++) {
77
instance_lists.push_back(instance_list_entry_bulk);
78
}
79
80
/**
81
* Fetch all
82
*/
83
int inserted_count = InstanceListRepository::InsertMany(instance_lists);
84
85
LogInfo("Bulk insertion test, inserted [{}]", inserted_count);
86
87
for (auto &entry: InstanceListRepository::GetWhere(fmt::format("zone = {}", 999))) {
88
LogInfo("Iterating through entry id [{}] zone [{}]", entry.id, entry.zone);
89
}
90
91
/**
92
* Delete where
93
*/
94
int deleted_count = InstanceListRepository::DeleteWhere(fmt::format("zone = {}", 999));
95
96
LogInfo("Bulk deletion test, deleted [{}]", deleted_count);
97
98
}
Copied!

Command Output

1
./bin/world test:repository
2
[MySQL Query] INSERT INTO instance_list (zone, version, is_global, start_time, duration, never_expires) VALUES (999,1,1,0,0,1) (1 row affected) (0.000942s)
3
[WorldServer] [Info] Inserted ID is [3669] zone [999]
4
[MySQL Query] SELECT id, zone, version, is_global, start_time, duration, never_expires FROM instance_list WHERE id = 3669 LIMIT 1 (1 row returned) (0.000274s)
5
[WorldServer] [Info] Found ID is [3669] zone [999]
6
[WorldServer] [Info] Updating instance id [3669] zone [999]
7
[MySQL Query] UPDATE instance_list SET zone = 999, version = 1, is_global = 1, start_time = 0, duration = 0, never_expires = 1 WHERE id = 3669 (1 row affected) (0.000188s)
8
[WorldServer] [Info] Updated instance id [3669] zone [777] affected [1]
9
[MySQL Query] DELETE FROM instance_list WHERE id = 3669 (1 row affected) (0.000676s)
10
[WorldServer] [Info] Deleting one instance [3669] deleted count [1]
11
[MySQL Query] INSERT INTO instance_list (zone, version, is_global, start_time, duration, never_expires) VALUES (999,1,1,0,0,1),(999,1,1,0,0,1),(999,1,1,0,0,1),(999,1,1,0,0,1),(999,1,1,0,0,1),(999,1,1,0,0,1),(999,1,1,0,0,1),(999,1,1,0,0,1),(999,1,1,0,0,1),(999,1,1,0,0,1) (10 rows affected) (0.000456s)
12
[WorldServer] [Info] Bulk insertion test, inserted [10]
13
[MySQL Query] SELECT id, zone, version, is_global, start_time, duration, never_expires FROM instance_list WHERE zone = 999 (10 rows returned) (0.000219s)
14
[WorldServer] [Info] Iterating through entry id [3670] zone [999]
15
[WorldServer] [Info] Iterating through entry id [3671] zone [999]
16
[WorldServer] [Info] Iterating through entry id [3672] zone [999]
17
[WorldServer] [Info] Iterating through entry id [3673] zone [999]
18
[WorldServer] [Info] Iterating through entry id [3674] zone [999]
19
[WorldServer] [Info] Iterating through entry id [3675] zone [999]
20
[WorldServer] [Info] Iterating through entry id [3676] zone [999]
21
[WorldServer] [Info] Iterating through entry id [3677] zone [999]
22
[WorldServer] [Info] Iterating through entry id [3678] zone [999]
23
[WorldServer] [Info] Iterating through entry id [3679] zone [999]
24
[MySQL Query] DELETE FROM instance_list WHERE zone = 999 (10 rows affected) (0.000459s)
25
[WorldServer] [Info] Bulk deletion test, deleted [10]
Copied!
You can see how we've had to use zero raw SQL to interact with the database in our actual domain logic; we can keep all of the SQL queries and interactions hidden behind methods created in our repositories and simply pass needed methods to those repository methods

Another Example

Another real use example is where we need some additional criteria to pull some results from the database. Instead of querying for grids by zone using raw SQL we want to encapsulate some of this so it can be easily used in our domain logic. We could use GetWhere quickly, but to make a more re-usable method we're going to create some new methods for our pathing grids
    static std::vector GetZoneGrids(int zone_id)
    static Grid GetGrid(const std::vector &grids, int grid_id)
grid_repository.h
1
2
// Custom extended repository methods here
3
4
static std::vector<Grid> GetZoneGrids(int zone_id)
5
{
6
std::vector<Grid> grids;
7
8
auto results = content_db.QueryDatabase(
9
fmt::format(
10
"{} WHERE zoneid = {}",
11
BaseSelect(),
12
zone_id
13
)
14
);
15
16
for (auto row = results.begin(); row != results.end(); ++row) {
17
Grid entry{};
18
19
entry.id = atoi(row[0]);
20
entry.zoneid = atoi(row[1]);
21
entry.type = atoi(row[2]);
22
entry.type2 = atoi(row[3]);
23
24
grids.push_back(entry);
25
}
26
27
return grids;
28
}
29
30
static Grid GetGrid(
31
const std::vector<Grid> &grids,
32
int grid_id
33
)
34
{
35
for (auto &row : grids) {
36
if (row.id == grid_id) {
37
return row;
38
}
39
}
40
41
return NewEntity();
42
}
Copied!
Since we want these records to be cached at the zone level; we create two properties to eventually load via data that is zone contextual
zone.h
1
class Zone {
2
...
3
std::vector<GridRepository::Grid> zone_grids;
4
std::vector<GridEntriesRepository::GridEntry> zone_grid_entries;
Copied!
We created a function that during zone initialization we call LoadGrids so we can reuse it in other parts of the code if we wanted to reload grid data for any reason
1
void Zone::LoadGrids()
2
{
3
zone_grids = GridRepository::GetZoneGrids(GetZoneID());
4
zone_grid_entries = GridEntriesRepository::GetZoneGridEntries(GetZoneID());
5
}
Copied!
Since we have this data in memory now using the structure directly mapped from the repositories, we also have this very nice way over iterating over the data in a vector when it comes to load the data into the grids / waypoints themselves
waypoints.cpp
1
void NPC::AssignWaypoints ...
2
...
3
4
for (auto &entry : zone->zone_grid_entries) {
5
if (entry.gridid == grid_id) {
6
wplist new_waypoint{};
7
new_waypoint.index = max_wp;
8
new_waypoint.x = entry.x;
9
new_waypoint.y = entry.y;
10
new_waypoint.z = entry.z;
11
new_waypoint.pause = entry.pause;
12
new_waypoint.heading = entry.heading;
13
new_waypoint.centerpoint = entry.centerpoint;
14
15
LogPathing(
16
"Loading Grid [{}] number [{}] name [{}]",
17
grid_id,
18
entry.number,
19
GetCleanName()
20
);
21
22
Waypoints.push_back(new_waypoint);
23
max_wp++;
24
}
25
}
Copied!
Last modified 5mo ago