Skip to content

Commit 3ec808b

Browse files
committed
feat(database): Add docs on database setup
1 parent e6ac34c commit 3ec808b

File tree

2 files changed

+283
-2
lines changed

2 files changed

+283
-2
lines changed

guides/database.md

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
---
2+
icon: database
3+
label: Database Setup
4+
---
5+
6+
Data storage is important when it comes to persistent loading and saving of data on your server.
7+
8+
There are many solutions that server use for data storage, but the most two most popular kind is persistent data storage with SQL or NoSQL databases. The simple different between these two are that SQL have structured tables and language for queries, whereas NoSQL utilizes documents (essentially JSON objects).
9+
10+
In our following examples, we will be using MongoDB, a NoSQL database. The reason for this is that MongoDB is a performant database solution, and by the nature of being NoSQL, it makes it extremely easy to perform queries and saving data. One of the big tooling the Go MongoDB library provides is type-safe data fetching so that all you need to do is make your own struct to represent your data.
11+
12+
## Setup
13+
14+
If you have not already done so, install MongoDB on your system and then install the Go package. The tutorial for this is below.
15+
16+
[!ref](/guides/setup.md)
17+
18+
## Data Folder Organization
19+
20+
In the previous setup tutorial to set up your project, we created a `minecraft` directory that held all of our server logic. In this folder, we will create a folder called `data` which will store all our files that are involved with our database and models.
21+
22+
## Database Entry Point
23+
24+
We want to create an entry point to our database so that it is loaded on the server start. To do this without much work, we can utilize Go's `init()` function, which will automatically run whenever the package is imported in any file associated from the main file.
25+
26+
For our current example, we will assume the server will save some type of user/player data, as such, we will make a users collection which will store the user data.
27+
28+
Create a `data.go` in the previously created `minecraft/data` folder, and use the following code:
29+
30+
```go minecraft/data/data.go
31+
package data
32+
33+
import (
34+
"context"
35+
"go.mongodb.org/mongo-driver/mongo"
36+
"go.mongodb.org/mongo-driver/mongo/options"
37+
)
38+
39+
// ctx returns a context.Context.
40+
func ctx() context.Context {
41+
return context.Background()
42+
}
43+
44+
// db is the Upper database session.
45+
var db *mongo.Database
46+
47+
const URI = "mongodb://localhost"
48+
49+
// init creates the Upper database connection.
50+
func init() {
51+
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(URI))
52+
if err != nil {
53+
panic(err)
54+
}
55+
db = client.Database("minecraft")
56+
57+
userCollection = db.Collection("users")
58+
}
59+
```
60+
61+
This code simply utilizes an `init()` function to load our database from a specific URI (in this case, we will be using localhost assuming it's a local database). We will then fetch/create a `minecraft` database which will hold the collection of our `users`.
62+
63+
!!! **Note**
64+
There is no need to check if the database nor collection are created, the methods on the database client and database will do this for us!
65+
!!!
66+
67+
## User Collection
68+
69+
You will notice that there is an error because we haven't created a `userCollection` database yet. This will be our next step. Create a `user.go` file in the same directory and use the following code:
70+
71+
!!! **Important**
72+
We will be explaining this code in chunks. Thus, when copying code down, make sure to paste everything code block in this section in the same `user.go` file as you read on.
73+
!!!
74+
75+
```go minecraft/data/user.go
76+
package data
77+
78+
import (
79+
"errors"
80+
"log"
81+
"strings"
82+
"sync"
83+
"time"
84+
85+
"go.mongodb.org/mongo-driver/bson"
86+
"go.mongodb.org/mongo-driver/mongo"
87+
)
88+
89+
var (
90+
userCollection *mongo.Collection
91+
userMu sync.Mutex
92+
users = map[string]User{}
93+
)
94+
```
95+
96+
Firstly, we will create our `userCollection` which will hold our actual collection tied to the database. We will then create a `users` map that holds cached users as we do not want to make unneeded calls to the database (along with this, a mutex to preserve concurrent safety).
97+
98+
## User Structure
99+
100+
Next, we want to create our actual `User` struct (note, you should have an error here for an unknown `User` struct, this is what we'll be making):
101+
102+
```go minecraft/data/user.go
103+
type User struct {
104+
XUID string
105+
DisplayName string
106+
Name string
107+
ExampleField int
108+
}
109+
```
110+
111+
This is an example `User` field that has very simple data fields, you can add more as you go on for your needs. Note that the specific examples covered in the later guides will provide you with data fields to use.
112+
113+
## User Saving
114+
115+
Next, we want to create a function that can manually save a user, to do this, we will using the follow code:
116+
117+
```go minecraft/data/user.go
118+
func saveUserData(u User) error {
119+
filter := bson.M{"name": bson.M{"$eq": u.Name}}
120+
update := bson.M{"$set": u}
121+
122+
res, _ := userCollection.UpdateOne(ctx(), filter, update)
123+
124+
if res.MatchedCount == 0 {
125+
_, _ = userCollection.InsertOne(ctx(), u)
126+
}
127+
128+
return nil
129+
}
130+
```
131+
132+
## Default User Helper
133+
134+
We will now create a small helper function to create a default user, which will just be a blank user:
135+
136+
```go minecraft/data/user.go
137+
func DefaultUser(name string) User {
138+
u := User{
139+
DisplayName: name,
140+
Name: strings.ToLower(name),
141+
}
142+
return u
143+
}
144+
```
145+
146+
## Data Flushing
147+
148+
Now want a simple lightweight system to store flush our users and store them in a cache as well as have a way to fetch cached users. We want to do this at an interval, so we will combine an `init()` function with a go-routine and tickers:
149+
150+
```go minecraft/data/user.go
151+
func cachedUserCheck(f func(User) bool) (User, bool) {
152+
userMu.Lock()
153+
defer userMu.Unlock()
154+
for _, u := range users {
155+
if f(u) {
156+
return u, true
157+
}
158+
}
159+
return User{}, false
160+
}
161+
162+
func FlushCache() {
163+
userMu.Lock()
164+
defer userMu.Unlock()
165+
for _, u := range users {
166+
err := saveUserData(u)
167+
if err != nil {
168+
log.Println("Error saving user data:", err)
169+
return
170+
}
171+
}
172+
}
173+
174+
func init() {
175+
t := time.NewTicker(5 * time.Minute)
176+
go func() {
177+
for range t.C {
178+
FlushCache()
179+
}
180+
}()
181+
}
182+
```
183+
184+
## Data Decoding
185+
186+
Before we get into loading and saving our players, we need to create a simple helper method that will decode our results from the MongoDB format (BSON). You don't really need to understand what goes on in this function, but bear in mind that additional modifications will be made to this in later guides:
187+
188+
```go minecraft/data/user.go
189+
unc decodeSingleUserResult(result *mongo.SingleResult) (User, error) {
190+
var u = DefaultUser("")
191+
192+
err := result.Decode(&u)
193+
if err != nil {
194+
return User{}, err
195+
}
196+
197+
userMu.Lock()
198+
defer userMu.Unlock()
199+
users[u.Name] = u
200+
201+
return u, nil
202+
}
203+
```
204+
205+
## User Loading
206+
207+
Let's now create functions to load our users. We will have two different functions to do this for our needs, `LoadUser` and `LoadUserOrCreate`. The names are self-explanatory where the latter will simply create a user if it's not already made (this is specifically designed for first-time users):
208+
209+
```go minecraft/data/user.go
210+
func LoadUserOrCreate(name string) (User, error) {
211+
if u, ok := cachedUserCheck(func(u User) bool {
212+
return u.Name == strings.ToLower(name)
213+
}); ok {
214+
return u, nil
215+
}
216+
217+
filter := bson.M{"name": bson.M{"$eq": strings.ToLower(name)}}
218+
219+
result := userCollection.FindOne(ctx(), filter)
220+
if err := result.Err(); err != nil {
221+
if errors.Is(err, mongo.ErrNoDocuments) {
222+
return DefaultUser(name), nil
223+
}
224+
return User{}, err
225+
}
226+
227+
u, err := decodeSingleUserResult(result)
228+
if err != nil {
229+
return User{}, err
230+
}
231+
232+
return u, nil
233+
}
234+
235+
func LoadUser(name string) (User, bool) {
236+
if u, ok := cachedUserCheck(func(u User) bool {
237+
return u.Name == strings.ToLower(name)
238+
}); ok {
239+
return u, true
240+
}
241+
242+
filter := bson.M{"name": bson.M{"$eq": strings.ToLower(name)}}
243+
244+
result := userCollection.FindOne(ctx(), filter)
245+
if err := result.Err(); err != nil {
246+
return User{}, false
247+
}
248+
u, err := decodeSingleUserResult(result)
249+
if err != nil {
250+
return User{}, false
251+
}
252+
253+
return u, true
254+
}
255+
```
256+
257+
## User Saving
258+
259+
Lastly, we need a way to save user data to the cache, we can do this with the method below:
260+
261+
```go minecraft/data/user.go
262+
func SaveUser(u User) {
263+
userMu.Lock()
264+
defer userMu.Unlock()
265+
users[u.Name] = u
266+
}
267+
```
268+
269+
!!!success **Congratulations!**
270+
You just made your first basic database system for Dragonfly! Note, that you will have to use the `data` package outside this package in order for the database to actually start!
271+
!!!
272+
273+
## Recap
274+
275+
A basic recap of the above database system:
276+
- Every 5 minutes, users in the cache are manually saved to the database to prevent performance issues from constantly
277+
- Loading users will first look into the user cache before making a query to the database, after which the decoding writes the data on the default user to return the actual user which needs to be loaded
278+
- Saving users will simply write to the cache which will be manually saved every 5 minutes as previously mentioned
279+
- If a user is not found or an error occurs, a blank user and false value is returned, allowing for proper handling
280+
281+
With this, you should have a flexible database system that is both performant and easy-to-use for whatever your needs are in Dragonfly. Remember that modifications to this file will be needed if you are following the specific server guides.

guides/setup.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func readConfig(log *slog.Logger) (server.Config, error) {
105105
We are not done quite yet! You will see that your program will error because we have not created our file yet to handle the custom configuration. To do so, create a folder called `minecraft` and in it, a file called `config.go` and use the following code:
106106

107107
```go minecraft/config.go
108-
package practice
108+
package minecraft
109109

110110
import (
111111
"github.com/df-mc/dragonfly/server"
@@ -143,4 +143,4 @@ You just made your first basic Dragonfly server! With this is mind, you should n
143143

144144
## Further Information
145145

146-
Please note that on the more specific tutorials on programming specific gamemodes, you will most likely modify certain behaviour in both the entry point file and the configuration file. Understand and analyze the behaviour of every chunk of code within these files so you have understanding of why you will change code in a later section
146+
Please note that on the more specific tutorials on programming specific gamemodes, you will most likely modify certain behavior in both the entry point file and the configuration file. Understand and analyze the behavior of every chunk of code within these files so you have understanding of why you will change code in a later section

0 commit comments

Comments
 (0)