Introduction

Quick Start

Get up and running with gmsv_mongo in minutes

Quick Start Guide

This guide will walk you through your first gmsv_mongo integration, from connecting to MongoDB to performing basic operations.

Your First Connection

Let's start with the basics. Create a new Lua file in your addon:

lua/autorun/server/sv_database.lua
-- Load the MongoDB module
require("mongo")

-- Create a global DB object for your addon
DB = {}

-- Connect to MongoDB
local client = MongoDB.Client("mongodb://localhost:27017")

if not client then
    error("Failed to connect to MongoDB!")
end

-- Get your database
DB.database = client:Database("my_gameserver")

-- Define your collections
DB.players = DB.database:Collection("players")
DB.logs = DB.database:Collection("logs")

print("✓ Database initialized successfully!")

Your server is now connected to MongoDB. The connection is persistent and uses connection pooling automatically.

Basic CRUD Operations

Now let's perform basic Create, Read, Update, and Delete operations.

Creating Documents (Insert)

-- Insert a single player document
hook.Add("PlayerInitialSpawn", "SavePlayer", function(ply)
    local steamid = ply:SteamID()
    local name = ply:Nick()
    
    -- Insert player data
    local id = DB.players:InsertOne({
        steamid = steamid,
        name = name,
        playtime = 0,
        first_join = os.time(),
        last_seen = os.time()
    })
    
    print("Saved player:", name, "with ID:", id)
end)

Reading Documents (Find)

-- Find a specific player
local function GetPlayerData(steamid)
    local playerData = DB.players:FindOne({ 
        steamid = steamid 
    })
    
    if playerData then
        print("Found player:", playerData.name)
        print("Playtime:", playerData.playtime, "hours")
        return playerData
    else
        print("Player not found!")
        return nil
    end
end

-- Find all players with high playtime
local function GetVeteranPlayers()
    local veterans = DB.players:Find({
        playtime = { ["$gte"] = 100 }  -- 100+ hours
    }, 10)  -- Limit to 10 results
    
    print("Found", #veterans, "veteran players")
    return veterans
end

Updating Documents

-- Update player's last seen time
hook.Add("PlayerDisconnect", "UpdatePlayerData", function(ply)
    local steamid = ply:SteamID()
    
    -- Update last seen time
    local updated = DB.players:UpdateOne(
        { steamid = steamid },
        { 
            ["$set"] = { 
                last_seen = os.time(),
                name = ply:Nick()  -- Update name in case they changed it
            }
        }
    )
    
    if updated > 0 then
        print("Updated player data for", ply:Nick())
    end
end)

-- Increment playtime every minute
timer.Create("UpdatePlaytime", 60, 0, function()
    for _, ply in ipairs(player.GetAll()) do
        DB.players:UpdateOne(
            { steamid = ply:SteamID() },
            { 
                ["$inc"] = { 
                    playtime = 1/60  -- 1 minute in hours
                }
            }
        )
    end
end)

Deleting Documents

-- Delete a specific player
local function DeletePlayer(steamid)
    local deleted = DB.players:DeleteOne({ 
        steamid = steamid 
    })
    
    if deleted > 0 then
        print("Player deleted successfully")
        return true
    else
        print("Player not found")
        return false
    end
end

-- Delete inactive players (not seen in 6 months)
local function CleanupInactivePlayers()
    local sixMonthsAgo = os.time() - (6 * 30 * 24 * 60 * 60)
    
    local deleted = DB.players:DeleteMany({
        last_seen = { ["$lt"] = sixMonthsAgo }
    })
    
    print("Deleted", deleted, "inactive players")
    return deleted
end

Working with Complex Data

MongoDB is great for storing complex, nested data structures:

-- Save player inventory
local function SaveInventory(ply)
    local steamid = ply:SteamID()
    local inventory = {}
    
    -- Build inventory table
    for _, weapon in ipairs(ply:GetWeapons()) do
        table.insert(inventory, {
            class = weapon:GetClass(),
            clip1 = weapon:Clip1(),
            clip2 = weapon:Clip2(),
            ammo = ply:GetAmmoCount(weapon:GetPrimaryAmmoType())
        })
    end
    
    -- Save to database
    DB.players:UpdateOne(
        { steamid = steamid },
        { 
            ["$set"] = { 
                inventory = inventory,
                health = ply:Health(),
                armor = ply:Armor(),
                position = {
                    x = ply:GetPos().x,
                    y = ply:GetPos().y,
                    z = ply:GetPos().z,
                    map = game.GetMap()
                }
            }
        },
        true  -- upsert: create if doesn't exist
    )
end

-- Load player inventory
local function LoadInventory(ply)
    local steamid = ply:SteamID()
    local data = DB.players:FindOne({ steamid = steamid })
    
    if not data or not data.inventory then
        print("No saved inventory for", ply:Nick())
        return
    end
    
    -- Restore inventory
    ply:StripWeapons()
    for _, item in ipairs(data.inventory) do
        local weapon = ply:Give(item.class)
        if IsValid(weapon) then
            weapon:SetClip1(item.clip1)
            weapon:SetClip2(item.clip2)
        end
    end
    
    -- Restore stats
    ply:SetHealth(data.health or 100)
    ply:SetArmor(data.armor or 0)
    
    print("Loaded inventory for", ply:Nick())
end

Query Operators

MongoDB supports powerful query operators:

-- Find players with specific criteria
local results = DB.players:Find({
    -- Greater than or equal
    playtime = { ["$gte"] = 50 },
    
    -- Less than
    level = { ["$lt"] = 10 },
    
    -- In array
    rank = { ["$in"] = { "VIP", "Admin", "Moderator" } },
    
    -- Not equal
    banned = { ["$ne"] = true },
    
    -- Exists
    email = { ["$exists"] = true },
    
    -- Regex pattern matching
    name = { ["$regex"] = "^Player" }  -- Names starting with "Player"
})

Update Operators

Update documents with powerful operators:

-- Various update operations
DB.players:UpdateOne(
    { steamid = "STEAM_0:1:12345" },
    {
        -- Set field value
        ["$set"] = { 
            rank = "VIP",
            vip_expires = os.time() + (30 * 24 * 60 * 60)  -- 30 days
        },
        
        -- Increment numeric value
        ["$inc"] = { 
            credits = 1000,
            level = 1
        },
        
        -- Push to array
        ["$push"] = { 
            achievements = "First_Kill"
        },
        
        -- Remove from array
        ["$pull"] = { 
            pending_tasks = "tutorial"
        },
        
        -- Set current timestamp
        ["$currentDate"] = { 
            last_modified = true
        }
    }
)

Counting Documents

-- Count total players
local totalPlayers = DB.players:Count({})
print("Total players:", totalPlayers)

-- Count active players
local activePlayers = DB.players:Count({
    last_seen = { ["$gte"] = os.time() - (7 * 24 * 60 * 60) }  -- Last 7 days
})
print("Active players:", activePlayers)

-- Count VIP players
local vipCount = DB.players:Count({ rank = "VIP" })
print("VIP players:", vipCount)

Bulk Operations

For better performance when working with multiple documents:

-- Insert multiple documents at once
local function InitializeDefaultItems()
    local defaultItems = {
        { name = "Health Kit", price = 50, category = "medical" },
        { name = "Armor Vest", price = 100, category = "armor" },
        { name = "Ammo Box", price = 25, category = "ammunition" },
    }
    
    local ids = DB.items:InsertMany(defaultItems)
    print("Inserted", #ids, "default items")
end

-- Update multiple players at once
local function ResetDailyBonus()
    local updated = DB.players:UpdateMany(
        {},  -- All players
        {
            ["$set"] = { 
                daily_bonus_claimed = false,
                daily_bonus_date = os.date("%Y-%m-%d")
            }
        }
    )
    
    print("Reset daily bonus for", updated, "players")
end

Error Handling

Always handle potential errors gracefully:

local function SafeDatabaseOperation()
    local success, result = pcall(function()
        return DB.players:FindOne({ steamid = "STEAM_0:1:12345" })
    end)
    
    if success then
        if result then
            print("Player found:", result.name)
            return result
        else
            print("Player not found")
            return nil
        end
    else
        print("Database error:", result)
        -- Log error, notify admin, etc.
        return nil
    end
end

Production-Ready: Async Operations

The examples above use synchronous operations for learning purposes. In production, you must use async operations to prevent server lag!

Why Async Matters

When you use sync operations, your entire server freezes while waiting for the database:

-- ❌ This freezes the server for 50-200ms!
local player = DB.players:FindOne({ steamid = ply:SteamID() })

With 20 players connecting simultaneously, that's 1-4 seconds of server freeze!

Converting to Async

Every operation has an async version that accepts a callback:

-- Async Insert
hook.Add("PlayerInitialSpawn", "SavePlayer", function(ply)
    DB.players:InsertOneAsync({
        steamid = ply:SteamID(),
        name = ply:Nick(),
        joined = os.time()
    }, function(err, id)
        if err then
            ErrorNoHalt("Failed to save player: " .. err .. "\n")
            return
        end
        print("✓ Saved player with ID:", id)
    end)
end)

-- Async Find
local function LoadPlayerData(ply)
    DB.players:FindOneAsync({ 
        steamid = ply:SteamID() 
    }, function(err, data)
        if err then
            ErrorNoHalt("Database error: " .. err .. "\n")
            return
        end
        
        if data then
            ply:SetNWInt("Money", data.money or 0)
            ply:SetNWInt("Level", data.level or 1)
        else
            -- New player
            CreateDefaultData(ply)
        end
    end)
end

-- Async Update
local function GiveMoney(ply, amount)
    DB.players:UpdateOneAsync(
        { steamid = ply:SteamID() },
        { ["$inc"] = { money = amount } },
        false, -- upsert
        function(err, count)
            if err then
                ErrorNoHalt("Failed to update: " .. err .. "\n")
                return
            end
            ply:ChatPrint("Received $" .. amount)
        end
    )
end

Available Async Operations

SyncAsyncCallback
InsertOne()InsertOneAsync()function(err, id)
InsertMany()InsertManyAsync()function(err, ids)
FindOne()FindOneAsync()function(err, doc)
Find()FindAsync()function(err, docs)
UpdateOne()UpdateOneAsync()function(err, count)
UpdateMany()UpdateManyAsync()function(err, count)
DeleteOne()DeleteOneAsync()function(err, count)
DeleteMany()DeleteManyAsync()function(err, count)
Count()CountAsync()function(err, count)
Aggregate()AggregateAsync()function(err, results)
For complete details on async operations, error handling, chaining callbacks, and best practices:Read the Async Operations Guide

What's Next?

Now that you understand the basics:

Core Concepts

Learn about Clients, Databases, and Collections

API Reference

Explore the complete API documentation

Advanced Features

Discover aggregation, indexes, and more

Examples

See real-world implementation examples