CRUD Operations

Async Operations

Non-blocking database operations with callbacks

Async Operations

Async operations allow your server to continue processing while database operations complete in the background.

Why Use Async?

Synchronous (Blocking)

-- This blocks the server until complete
local result = collection:Find({})  -- Server pauses here
-- Other players experience lag

Asynchronous (Non-Blocking)

-- This returns immediately
collection:FindAsync({}, nil, function(err, result)
    -- Called when complete
end)
-- Server continues running

Callback Pattern

All async operations use callbacks with this signature:

function(err, result)
    if err then
        -- Handle error
    else
        -- Process result
    end
end

Available Async Methods

Insert Operations

InsertOneAsync

collection:InsertOneAsync(document, function(err, insertedId)
    if err then
        print("Insert error:", err)
        return
    end
    print("Inserted with ID:", insertedId)
end)

InsertManyAsync

collection:InsertManyAsync(documents, function(err, insertedIds)
    if err then
        print("Insert error:", err)
        return
    end
    print("Inserted", #insertedIds, "documents")
end)

Query Operations

FindAsync

collection:FindAsync(filter, limit, callback)
Important:FindAsync requires a limit argument between the filter and the callback. If you omit it (or pass 0), no documents will be returned and your callback will never receive any results. Always pass the maximum number of documents you want returned, or use nil to return all matching documents (not recommended for large datasets).
-- Find up to 100 documents matching a filter
collection:FindAsync({ level = { ["$gte"] = 10 } }, 100, function(err, documents)
    if err then
        print("Find error:", err)
        return
    end

    print("Found", #documents, "documents")
    for _, doc in ipairs(documents) do
        print("  -", doc.username)
    end
end)

-- Find all players (use a high limit to retrieve all documents)
collection:FindAsync({}, 1000, function(err, documents)
    if err then
        print("Find error:", err)
        return
    end

    print("Total players retrieved:", #documents)
end)

FindOneAsync

collection:FindOneAsync({ steamid = steamid }, function(err, document)
    if err then
        print("Find error:", err)
        return
    end

    if document then
        print("Found:", document.username)
    else
        print("Not found")
    end
end)

CountAsync

collection:CountAsync({ active = true }, function(err, count)
    if err then
        print("Count error:", err)
        return
    end
    print("Active players:", count)
end)

Update Operations

UpdateOneAsync

collection:UpdateOneAsync(
    { steamid = steamid },
    { ["$inc"] = { credits = 100 } },
    function(err, modifiedCount)
        if err then
            print("Update error:", err)
            return
        end
        print("Modified:", modifiedCount)
    end
)

UpdateManyAsync

collection:UpdateManyAsync(
    { vip = true },
    { ["$inc"] = { bonus = 50 } },
    function(err, modifiedCount)
        if err then
            print("Update error:", err)
            return
        end
        print("Updated", modifiedCount, "VIP players")
    end
)

Delete Operations

DeleteOneAsync

collection:DeleteOneAsync(
    { steamid = steamid },
    function(err, deletedCount)
        if err then
            print("Delete error:", err)
            return
        end
        if deletedCount > 0 then
            print("Deleted successfully")
        end
    end
)

DeleteManyAsync

collection:DeleteManyAsync(
    { expired = true },
    function(err, deletedCount)
        if err then
            print("Delete error:", err)
            return
        end
        print("Deleted", deletedCount, "expired records")
    end
)

Aggregation

AggregateAsync

collection:AggregateAsync({
    {
        ["$group"] = {
            _id = "$class",
            count = { ["$sum"] = 1 }
        }
    }
}, function(err, results)
    if err then
        print("Aggregation error:", err)
        return
    end

    for _, result in ipairs(results) do
        print(result._id, ":", result.count)
    end
end)

Practical Examples

Async Player Loading

function LoadPlayerAsync(steamid, callback)
    local players = db:Collection("players")

    players:FindOneAsync({ steamid = steamid }, function(err, player)
        if err then
            callback(nil, err)
            return
        end

        if player then
            -- Player exists, update login time
            players:UpdateOneAsync(
                { steamid = steamid },
                { ["$set"] = { last_login = os.time() } },
                function() end  -- Fire and forget
            )
            callback(player, nil)
        else
            -- New player, create record
            local newPlayer = {
                steamid = steamid,
                level = 1,
                credits = 1000,
                created_at = os.time()
            }
            players:InsertOneAsync(newPlayer, function(err, id)
                if err then
                    callback(nil, err)
                else
                    newPlayer._id = id
                    callback(newPlayer, nil)
                end
            end)
        end
    end)
end

-- Usage in GMod
hook.Add("PlayerInitialSpawn", "LoadPlayerData", function(ply)
    local steamid = ply:SteamID()

    LoadPlayerAsync(steamid, function(player, err)
        if err then
            print("Failed to load player:", err)
            return
        end

        -- Store data on player entity
        ply.dbData = player
        ply:SetNWInt("Level", player.level)
        ply:SetNWInt("Credits", player.credits)

        print("Loaded data for", ply:Nick())
    end)
end)

Async Save System

function SavePlayerAsync(steamid, data, callback)
    local players = db:Collection("players")

    players:UpdateOneAsync(
        { steamid = steamid },
        { ["$set"] = data },
        function(err, modified)
            if callback then
                callback(err, modified > 0)
            end
        end
    )
end

-- Auto-save every 5 minutes
timer.Create("AutoSave", 300, 0, function()
    for _, ply in ipairs(player.GetAll()) do
        if ply.dbData then
            SavePlayerAsync(ply:SteamID(), {
                credits = ply.dbData.credits,
                level = ply.dbData.level,
                playtime = ply.dbData.playtime + 300
            })
        end
    end
    print("Auto-save complete")
end)

Async Leaderboard

function GetLeaderboardAsync(callback)
    local players = db:Collection("players")

    players:AggregateAsync({
        { ["$match"] = { banned = { ["$ne"] = true } } },
        { ["$sort"] = { score = -1 } },
        { ["$limit"] = 10 },
        { ["$project"] = {
            username = 1,
            score = 1,
            level = 1,
            _id = 0
        }}
    }, function(err, results)
        if err then
            callback(nil, err)
        else
            callback(results, nil)
        end
    end)
end

-- Usage
GetLeaderboardAsync(function(leaderboard, err)
    if err then
        print("Failed to get leaderboard:", err)
        return
    end

    print("=== Top 10 Players ===")
    for i, entry in ipairs(leaderboard) do
        print(string.format("%d. %s - %d points",
            i, entry.username, entry.score))
    end
end)

Fire-and-Forget Logging

function LogEventAsync(category, message, data)
    local logs = db:Collection("logs")

    logs:InsertOneAsync({
        category = category,
        message = message,
        data = data or {},
        timestamp = os.time()
    }, function(err)
        -- Optional: log failures to console
        if err then
            print("[LOG ERROR]", err)
        end
    end)
end

-- Usage - doesn't block game
LogEventAsync("player", "Player connected", { steamid = "STEAM_0:1:12345" })
LogEventAsync("economy", "Purchase made", { item = "weapon", cost = 500 })
LogEventAsync("combat", "Player killed", { killer = "A", victim = "B" })

Error Handling Patterns

Basic Error Handling

collection:FindOneAsync({ steamid = steamid }, function(err, result)
    if err then
        print("Database error:", err)
        -- Handle error (notify player, retry, etc.)
        return
    end

    -- Process result
end)

Retry Pattern

function FindWithRetry(collection, filter, maxRetries, callback)
    local attempts = 0

    local function attempt()
        attempts = attempts + 1
        collection:FindOneAsync(filter, function(err, result)
            if err and attempts < maxRetries then
                print("Retrying... attempt", attempts + 1)
                timer.Simple(1, attempt)  -- Wait 1 second before retry
            else
                callback(err, result)
            end
        end)
    end

    attempt()
end

-- Usage
FindWithRetry(players, { steamid = steamid }, 3, function(err, player)
    if err then
        print("Failed after 3 attempts:", err)
    else
        print("Found player:", player.username)
    end
end)

Callback Wrapper

function SafeCallback(callback)
    return function(err, result)
        local success, error = pcall(callback, err, result)
        if not success then
            print("Callback error:", error)
        end
    end
end

-- Usage
collection:FindOneAsync(filter, SafeCallback(function(err, result)
    -- Your callback code
end))

Best Practices

  1. Use async for gameplay logic: Prevents server lag
  2. Handle all errors: Never ignore the error parameter
  3. Use fire-and-forget for logging: Non-critical operations
  4. Avoid callback hell: Structure code cleanly
  5. Consider timeouts: Handle slow operations
-- Avoid deep nesting (callback hell)
-- Bad:
collection:FindAsync(filter, 100, function(err, results)
    if not err then
        collection:UpdateAsync(filter2, update, function(err, result)
            if not err then
                collection:FindAsync(filter3, 100, function(err, results)
                    -- Too deep!
                end)
            end
        end)
    end
end)

-- Better: Use separate functions
local function step3(data)
    collection:FindAsync(data.filter, 100, function(err, results)
        -- Handle final step
    end)
end

local function step2(data)
    collection:UpdateAsync(data.filter, data.update, function(err, result)
        if not err then step3(data) end
    end)
end

local function step1(data)
    collection:FindAsync(data.filter, 100, function(err, results)
        if not err then step2(data) end
    end)
end

Next Steps