|
| 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. |
0 commit comments