最佳实践
本指南提供了使用 Sleet ORM 的最佳实践,帮助您构建高效、可维护的 FiveM 应用。
Schema 设计原则
合理的表结构设计
lua
-- ✅ 好的实践
local players = sl.table('players', {
-- 使用有意义的主键
id = sl.serial().primaryKey().comment('玩家唯一标识'),
-- 唯一标识符应该有约束
identifier = sl.varchar(64).notNull().unique().comment('Steam/Discord ID'),
-- 金钱字段使用 decimal 确保精度
money = sl.decimal(10, 2).default(1000.00).comment('现金金额'),
bank = sl.decimal(12, 2).default(5000.00).comment('银行余额'),
-- 布尔字段提供默认值
is_active = sl.boolean().default(true).comment('账户是否激活'),
-- 时间戳字段使用自动更新
created_at = sl.timestamp().defaultNow().comment('创建时间'),
updated_at = sl.timestamp().defaultNow().onUpdate(sl.sql('NOW()')).comment('更新时间'),
-- 软删除支持
deleted_at = sl.timestamp().softDelete().comment('删除时间戳')
})
-- ❌ 避免的做法
local bad_players = sl.table('players', {
-- 没有注释,难以理解
id = sl.int(),
-- 金钱使用 int,可能丢失精度
money = sl.int(),
-- 缺少约束和默认值
name = sl.varchar(255),
active = sl.boolean()
})数据类型选择
lua
-- 根据实际需求选择合适的数据类型
local items = sl.table('items', {
id = sl.serial().primaryKey(),
-- 字符串长度要合理
name = sl.varchar(100).notNull().comment('物品名称'), -- 短名称
description = sl.text().comment('详细描述'), -- 长文本
-- 数字类型要精确
price = sl.decimal(8, 2).comment('价格(精确到分)'), -- 货币
weight = sl.float().comment('重量(公斤)'), -- 浮点数
stack_size = sl.smallint().default(1).comment('堆叠数量'), -- 小整数
-- 合理使用 JSON
metadata = sl.json().comment('扩展属性'), -- 复杂数据
-- 时间字段
expires_at = sl.timestamp().comment('过期时间'),
created_at = sl.timestamp().defaultNow()
})查询优化
使用索引友好的查询
lua
-- ✅ 索引友好的查询
local player = db.select()
.from(s.players)
.where(sl.eq(s.players.identifier, identifier)) -- identifier 有 unique 索引
.limit(1)
.execute()[1]
-- ✅ 复合条件查询
local activePlayers = db.select()
.from(s.players)
.where(sl.and_(
sl.eq(s.players.is_active, true),
sl.isNull(s.players.deleted_at)
))
.execute()
-- ❌ 避免全表扫描
local badQuery = db.select()
.from(s.players)
.where(sl.like(s.players.metadata, '%某个值%')) -- JSON 字段模糊查询
.execute()分页查询
lua
-- ✅ 高效的分页
function GetPlayersPaginated(page, pageSize)
pageSize = pageSize or 20
local offset = (page - 1) * pageSize
return db.select()
.from(s.players)
.orderBy(s.players.id, 'asc') -- 稳定的排序
.limit(pageSize)
.offset(offset)
.execute()
end
-- ✅ 获取总数(如果需要)
function GetPlayersCount()
---@type { count: integer }[]
local result = db.select({ sl.sql('COUNT(*) as count') })
.from(s.players)
.execute()
return result[1].count
end批量操作
lua
-- ✅ 批量插入
function CreateMultiplePlayers(playersData)
return db.insert(s.players)
.values(playersData) -- 传入数组
.execute()
end
-- ✅ 批量更新(使用 IN 查询)
function UpdatePlayersMoney(playerIds, amount)
return db.update(s.players)
.set({ money = sl.sql('`money` + ?', { amount }) })
.where(sl.inArray(s.players.id, playerIds))
.execute()
end错误处理
安全的数据库操作
lua
-- ✅ 带错误处理的操作
function SafeUpdatePlayerMoney(playerId, amount)
local success, result = pcall(function()
return db.update(s.players)
.set({ money = sl.sql('`money` + ?', { amount }) })
.where(sl.and_(
sl.eq(s.players.id, playerId),
sl.gte(s.players.money, -amount) -- 防止负数
))
.execute()
end)
if not success then
print("更新玩家金钱失败:", result)
return false
end
return result > 0 -- 返回是否实际更新了记录
end
-- ✅ 验证数据存在性
function GetPlayerSafely(identifier)
local players = db.select()
.from(s.players)
.where(sl.eq(s.players.identifier, identifier))
.limit(1)
.execute()
if #players == 0 then
return nil, "玩家不存在"
end
return players[1], nil
end性能优化
连接管理
lua
-- ✅ 复用数据库连接
local db = sl.connect() -- 在模块顶层创建一次
-- ✅ 避免在循环中重复创建连接
function ProcessPlayers(playerList)
for _, playerId in ipairs(playerList) do
-- 使用已有的 db 连接
local player = db.select()
.from(s.players)
.where(sl.eq(s.players.id, playerId))
.execute()[1]
-- 处理玩家数据...
end
end查询缓存策略
lua
-- ✅ 简单的内存缓存
local playerCache = {}
local CACHE_TTL = 300000 -- 5分钟
function GetPlayerCached(identifier)
local cached = playerCache[identifier]
if cached and (GetGameTimer() - cached.timestamp) < CACHE_TTL then
return cached.data
end
-- 从数据库获取
local player = db.select()
.from(s.players)
.where(sl.eq(s.players.identifier, identifier))
.execute()[1]
if player then
playerCache[identifier] = {
data = player,
timestamp = GetGameTimer()
}
end
return player
end减少数据传输
lua
-- ✅ 只查询需要的字段
function GetPlayerBasicInfo(identifier)
return db.select({
s.players.id,
s.players.name,
s.players.money,
s.players.bank
})
.from(s.players)
.where(sl.eq(s.players.identifier, identifier))
.execute()[1]
end
-- ❌ 避免查询不必要的大字段
function BadGetPlayerInfo(identifier)
-- 这会查询包括 metadata (JSON) 在内的所有字段
return db.select()
.from(s.players)
.where(sl.eq(s.players.identifier, identifier))
.execute()[1]
end数据完整性
原子操作
lua
-- ✅ 使用 SQL 表达式确保原子性
function TransferMoneyAtomic(fromId, toId, amount)
-- 扣除发送方
local fromResult = db.update(s.players)
.set({ money = sl.sql('`money` - ?', { amount }) })
.where(sl.and_(
sl.eq(s.players.id, fromId),
sl.gte(s.players.money, amount) -- 确保余额充足
))
.execute()
if fromResult == 0 then
return false, "余额不足或玩家不存在"
end
-- 增加接收方
local toResult = db.update(s.players)
.set({ money = sl.sql('`money` + ?', { amount }) })
.where(sl.eq(s.players.id, toId))
.execute()
if toResult == 0 then
-- 回滚:如果接收方不存在,撤销扣款
db.update(s.players)
.set({ money = sl.sql('`money` + ?', { amount }) })
.where(sl.eq(s.players.id, fromId))
.execute()
return false, "接收方不存在"
end
return true, "转账成功"
end数据验证
lua
-- ✅ 在应用层验证数据
function ValidatePlayerData(data)
if not data.identifier or type(data.identifier) ~= 'string' then
return false, "标识符无效"
end
if not data.name or type(data.name) ~= 'string' or #data.name > 255 then
return false, "名称无效"
end
if data.money and (type(data.money) ~= 'number' or data.money < 0) then
return false, "金钱数额无效"
end
return true, nil
end
function CreatePlayer(data)
local valid, error = ValidatePlayerData(data)
if not valid then
return nil, error
end
return db.insert(s.players)
.values(data)
.execute()
end代码组织
模块化设计
lua
-- server/database/players.lua
local sl = Sleet
local s = require 'server.schema'
local db = sl.connect()
local Players = {}
function Players.GetByIdentifier(identifier)
return db.select()
.from(s.players)
.where(sl.eq(s.players.identifier, identifier))
.limit(1)
.execute()[1]
end
function Players.Create(data)
return db.insert(s.players)
.values(data)
.execute()
end
function Players.UpdateMoney(playerId, amount)
return db.update(s.players)
.set({ money = sl.sql('`money` + ?', { amount }) })
.where(sl.eq(s.players.id, playerId))
.execute()
end
return Playerslua
-- server/main.lua
local Players = require 'server.database.players'
RegisterNetEvent('sleet:getPlayer', function()
local source = source
local identifier = GetPlayerIdentifierByType(source, 'steam')
local player = Players.GetByIdentifier(identifier)
if not player then
player = Players.Create({
identifier = identifier,
name = GetPlayerName(source)
})
end
-- 使用玩家数据...
end)类型安全
lua
-- ✅ 使用类型注解(如果支持 LuaLS)
---@param identifier string Steam 标识符
---@param name string 玩家名称
---@return integer|nil playerId 新创建的玩家 ID
function CreatePlayer(identifier, name)
return db.insert(s.players)
.values({
identifier = identifier,
name = name
})
.execute()
end
-- ✅ 为复杂查询添加返回类型注解
---@return { id: integer, name: string, money: number }[]
function GetRichPlayers()
return db.select({ s.players.id, s.players.name, s.players.money })
.from(s.players)
.where(sl.gte(s.players.money, 100000))
.execute()
end调试技巧
查询调试
lua
-- ✅ 调试模式下记录 SQL
local DEBUG_MODE = true
function DebugQuery(queryBuilder)
if DEBUG_MODE then
local sql, params = queryBuilder.toSQL()
print("SQL:", sql)
print("Params:", json.encode(params))
end
return queryBuilder.execute()
end
-- 使用方式
local players = DebugQuery(
db.select()
.from(s.players)
.where(sl.eq(s.players.is_active, true))
)性能监控
lua
-- ✅ 监控慢查询
function MonitoredQuery(name, queryFn)
local start = GetGameTimer()
local result = queryFn()
local duration = GetGameTimer() - start
if duration > 100 then -- 超过100ms记录警告
print(("慢查询警告: %s 耗时 %dms"):format(name, duration))
end
return result
end
-- 使用方式
local players = MonitoredQuery("获取活跃玩家", function()
return db.select()
.from(s.players)
.where(sl.eq(s.players.is_active, true))
.execute()
end)总结
遵循这些最佳实践将帮助您:
- 构建高性能的数据库操作
- 保证数据完整性和安全性
- 编写可维护的代码
- 避免常见的性能陷阱
记住,好的数据库设计和查询优化是构建稳定 FiveM 服务器的基础!
