【仓颉语言入门 · 第二篇 第 10 课】 【仓颉语言入门 · 第二篇 第 10 课】插入INSERT、占位符与自增主键上一课我们学会了 SELECT把sys_user里的 9 行用户数据读了个遍。增删改查里读之外最高频的就是写——往数据库里塞数据。这一课只做三件事发出第一条 INSERT、用set()给 SQL 里的?占位符填值、把数据库自动编的自增主键拿回来。目录第二篇导航第一篇《仓颉语言入门》共 30 课已完结环境搭建 → 语法 → 函数 → 类型系统 → 工程化 → 并发 → 两场实战。第二篇在更高的起点上继续共26 课、4 个模块模块课次内容一、补遗篇0106正则、时间日期、命令行参数、随机数、加密哈希、二进制处理二、数据库篇0715入门与连接、建表、查询、插入、更新删除、预编译防注入、事务、手写 DAO、综合小实战三、进阶硬核篇1622反射、宏基础、deriving 实战、FFI 基础、FFI 进阶、并发容器、对象池四、实战项目篇2326HTTP 基础、HTTP 进阶、多线程日志分析器、简易爬虫正则表达式std.regex时间日期处理std.time命令行参数解析std.argopt随机数与概率std.random加密与哈希std.crypto二进制数据处理std.binary数据库入门认识 std.database.sql连上 MySQL建表用代码定义表结构查询SELECT 与结果集遍历插入INSERT、占位符与自增主键本文更新与删除UPDATE / DELETE预编译与防 SQL 注入事务批量操作的成败一致手写 DAO 数据访问层数据库综合小实战反射在运行时打量类型宏基础std.ast 与 quotederiving 宏实战与 C 互操作基础FFI 调用 C 函数FFI 进阶结构体、指针与编译配置并发容器并发安全的集合对象池与资源复用实战三HTTP 服务端基础实战四HTTP 进阶与 JSON 接口实战五多线程日志分析器实战六简易爬虫一、先在客户端里把 INSERT 试明白数据库开发的好习惯先把 SQL 在客户端里试对再搬进代码。第 8 课建的练习表sys_login_log已经随练习删掉了先重建它USEtumo_governance;CREATETABLEIFNOTEXISTSsys_login_log(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULL,login_ipVARCHAR(50),login_timeDATETIME,successINTNOTNULLDEFAULT1);然后插一条字面量进去INSERTINTOsys_login_log(user_id,login_ip,login_time,success)VALUES(1900010622019957256,127.0.0.1,2026-07-30 22:08:44,1);用 SELECT 看一眼结果SELECT*FROMsys_login_log;------------------------------------------------------------------ | id | user_id | login_ip | login_time | success | ------------------------------------------------------------------ | 1 | 1900010622019957256 | 127.0.0.1 | 2026-07-30 22:08:44 | 1 | ------------------------------------------------------------------四个要点这就是 INSERT 的全部核心语法INSERT INTO 表名 (列清单)括号里列顺序你自己定不一定要和CREATE TABLE里的顺序一致但后面 VALUES 的值顺序必须和这里一一对应。VALUES (...)每个值按顺序填入列清单字符串用单引号包住。id没写因为它是AUTO_INCREMENT数据库自己编了个1下一行再插自动变成2。DEFAULT 1没写success列有DEFAULT 1省略它就自动填 1。试完在客户端里把它删掉下面用仓颉重新插不是偷懒是确保程序跑在干净状态DELETEFROMsys_login_log;注意这里的DELETE FROM只删数据不删表。它让表回到刚建好时的空表状态下一节的程序才能拿到lastInsertId 1。二、仓颉代码第一条 INSERT字面量直接写沿用cj_mysql_demo工程把src/main.cj改成package cj_mysql_demo import std.database.sql.* import mariadb.cdbc; main(): Int64 { let driver DriverManager.getDriver(mariadb).getOrThrow() let ds driver.open(mariadb://127.0.0.1:3306, [ (username, root), (password, wangpeng), (database, tumo_governance) ]) let conn ds.connect() let stmt conn.prepareStatement( INSERT INTO sys_login_log (user_id, login_ip, login_time, success) VALUES (1900010622019957256, 127.0.0.1, 2026-07-30 22:08:44, 1) ) let result stmt.update() println(插入完成影响行数${result.rowCount}自增主键${result.lastInsertId}) stmt.close() conn.close() 0 }运行输出插入完成影响行数1自增主键1三个要点DML 走update()INSERT、UPDATE、DELETE 这些改数据的语句和建表一样用update()发送返回值是UpdateResult第 8 课见过。rowCount数的是受影响的行插入 1 行它就是1插入 0 行或失败时异常早就被抛了到不了println。lastInsertId是自增主键的值AUTO_INCREMENT列数据库自动编的号通过UpdateResult.lastInsertId原样拿回——这里返回1表示数据库刚给这行分配的 id。这样写没问题但 SQL 里的值全拼死在字符串里了换个人名就得改代码重编译。下一节用占位符把它解耦出来。三、占位符让 SQL 安全又灵活真实项目里插入的数据都是程序运行时才算出来的表单输入、传感器读数、接口返回值……不可能提前写死在 SQL 字符串里。直接把值拼进 SQL 字符串不仅麻烦还有严重的安全隐患下节课讲 SQL 注入时会细说。正确做法是占位符SQL 里写?程序运行时用set()把值填进去。把刚才的 INSERT 改成占位符写法let stmt conn.prepareStatement( INSERT INTO sys_login_log (user_id, login_ip, login_time, success) VALUES (?, ?, ?, ?) ) stmt.set(1, 1900010622019957256) stmt.set(2, 127.0.0.1) stmt.set(3, DateTime.of(year: 2026, month: 7, dayOfMonth: 30, hour: 22, minute: 8, second: 44)) stmt.set(4, 1) let result stmt.update() println(插入完成rowCount${result.rowCount}lastInsertId${result.lastInsertId})运行输出插入完成rowCount1lastInsertId2为什么是2因为第一节和第二节已经各插过一行表没删id 接着编。如果你在客户端执行了DELETE FROM sys_login_log;则会变回1——lastInsertId只和当前表里的自增计数器有关。四个要点?按顺序占位SQL 里写几个?后面就要set几个值第一个?对应第 1 个set第二个对应第 2 个以此类推。set从 1 开始和第 9 课的get(1)完全一致从1计数。写set(0, ...)会抛IllegalArgumentException等下第五节给你看。类型自动匹配set是泛型函数驱动会按你传入的值类型自动处理——Int64给整型、String给字符串、DateTime给时间列、Int32给普通整数。基本不需要写尖括号和上一课的getString(1)那种显式写法不同。列可以省略走默认值success列有DEFAULT 1如果你不确定要不要填可以把它从 SQL 的列清单和VALUES里一起删掉数据库会自动填 1。试试看let stmt conn.prepareStatement( INSERT INTO sys_login_log (user_id, login_ip, login_time) VALUES (?, ?, ?) ) stmt.set(1, 1900010644348391560) stmt.set(2, 192.168.1.23) stmt.set(3, DateTime.now()) let result stmt.update() println(省略 successrowCount${result.rowCount}lastInsertId${result.lastInsertId})运行输出省略 successrowCount1lastInsertId3查询回去验证这行的success列确实是1DEFAULT 生效。可空列想主动写 NULL用setNull(序号)let stmt conn.prepareStatement( INSERT INTO sys_login_log (user_id, login_ip, login_time) VALUES (?, ?, ?) ) stmt.set(1, 1900010653277109512) stmt.setNull(2) stmt.set(3, DateTime.now()) let result stmt.update() println(setNull iprowCount${result.rowCount}lastInsertId${result.lastInsertId})运行输出setNull iprowCount1lastInsertId4查询确认这行的login_ip是 NULL和空字符串是两回事第 9 课讲过。四、自增主键与 lastInsertId练习表 5 列但程序通常只关心 4 列——id由数据库自动生成不用管。插入后立刻读lastInsertId就能拿到数据库给这行分配的编号后续用它关联、查询、打印都方便。连插两条看lastInsertId怎么长let stmt conn.prepareStatement( INSERT INTO sys_login_log (user_id, login_ip) VALUES (?, ?) ) stmt.set(1, 1900010680132446728) stmt.set(2, 172.16.0.2) var result stmt.update() println(第一条 lastInsertId${result.lastInsertId}) stmt.set(1, 1900010722168889096) stmt.set(2, 10.1.1.5) result stmt.update() println(第二条 lastInsertId${result.lastInsertId})运行输出接续前面的 id第一条 lastInsertId5 第二条 lastInsertId6同一个stmt复用时每次update()后参数会被清空——所以第二条 insert 前必须重新set否则参数全变 NULL往 NOT NULL 列插 NULL 会报错下一节看真实报错。如果只发一条 SQL 同时插多行lastInsertId 是最后一行的 idlet stmt conn.prepareStatement( INSERT INTO sys_login_log (user_id, login_ip) VALUES (?, ?), (?, ?) ) stmt.set(1, 1900010821105162120) stmt.set(2, 10.1.1.6) stmt.set(3, 1900010933547430664) stmt.set(4, 10.1.1.7) let result stmt.update() println(多行插入 rowCount${result.rowCount}lastInsertId${result.lastInsertId})运行输出多行插入 rowCount2lastInsertId8两行受影响lastInsertId 是最后一行的编号这里是8。rowCount才是你判断插了几行的可靠依据。注意lastInsertId只在插入目标表有AUTO_INCREMENT列时才有意义。如果表没有自增列比如第 9 课的sys_user它的id是程序自己算的分布式 IDlastInsertId恒为0。五、常见报错速览插入阶段的报错集中在值没给够和序号写错两类提前混个脸熟序号从 0 开始和get一样从 1 计数IllegalArgumentException: The parameter index must be greater than 0NOT NULL 列没给值两幅面孔如果列压根没出现在列清单里SqlException: Field user_id doesnt have a default value, errorCode: 0如果列清单里写了但 set 之后 update 没传值比如复用 stmt 忘了重新 setSqlException: Column user_id cannot be null, errorCode: 0两种说法不同根因一样程序没给值数据库拒绝写入。解决思路也一样检查set是否覆盖到每个?。同一个 stmt 复用不重新 setstmt.set(1, 1001) stmt.set(2, ok) stmt.update() // 第一次 OK stmt.update() // 第二次参数已被清空全变 NULL第二次执行会报Column user_id cannot be null。想要复用每次update()前把参数重新set一遍。六、常用 API 速查插入篇任务写法执行 INSERTconn.prepareStatement(sql).update()影响行数UpdateResult.rowCount取自增主键UpdateResult.lastInsertIdInt64填占位符stmt.set(序号, 值)序号从 1 开始填 NULLstmt.setNull(序号)省略列走 DEFAULT列名和?都不写该列复用同一个 stmt每次update()前重新set七、常见问题 FAQQ1set的序号从 0 开始还是从 1 开始从 1 开始和第 9 课getT(1)的列序号规则完全一致。写 0 会抛IllegalArgumentException。多占位符时序号连续递增即可。Q2只插入部分列其余列会怎样三种结局该列有DEFAULT如success INT DEFAULT 1→ 自动填默认值该列允许空没写NOT NULL→ 自动填NULL该列NOT NULL且没有 DEFAULT → 插入失败抛SqlException。Q3lastInsertId在表没有自增列时是什么恒为0。它不是插入后表里最大 id而是本次自增值——没有自增列就没有值可回传。Q4同一个prepareStatement能复用吗能但每执行一次update()后内部参数会被清空。想再执行一次必须重新set所有占位符。如果复用频繁注意检查 set 是否遗漏。八、课后练习前 3 题必做第 4 题选做。都在tumo_governance库的sys_login_log表上操作如果表已被删除先用课文的 CREATE TABLE IF NOT EXISTS 重建。第 1 题必做用占位符插入一条完整日志在cj_mysql_demo工程里用 4 个?占位符插入一行完整数据user_id 给1900010933650395016login_ip 给192.168.1.88login_time 给DateTime.now()success 给0然后打印rowCount和lastInsertId。提示SQL 为INSERT INTO sys_login_log (user_id, login_ip, login_time, success) VALUES (?, ?, ?, ?)四个stmt.set(...)依次填值success 给0表示登录失败。期望输出你的 lastInsertId 可能和示例不同取决于表里已有几行只要非 0 即可插入完成rowCount1lastInsertId9第 2 题必做故意触发序号越界写一条INSERT INTO sys_login_log (user_id) VALUES (?)然后执行stmt.set(0, 1001)运行确认报错逐字读一遍异常信息。提示这道题的锻炼目标是见过报错就会修——以后代码里序号写错时看到The parameter index must be greater than 0立刻知道该改序号。期望输出IllegalArgumentException: The parameter index must be greater than 0第 3 题必做验证 DEFAULT 生效不插入success列SQL 只写user_id、login_ip、login_time三列运行后用 SELECT 查回这行数据确认success的值是1。提示查询部分直接搬第 9 课的写法stmt.query()while (result.next())getInt32(列序号)查SELECT id, user_id, success FROM sys_login_log ORDER BY id DESC LIMIT 1只取最新一行。期望输出id 号随环境不同id10 uid1900010933650395016 success1第 4 题选做插入一条 login_ip 为 NULL 的记录并验证用setNull(2)把第二列login_ip置为 NULL插入后用 SELECT 查回来打印 id 和 ip确认 ip 显示为 “NULL”。提示login_ip是可空列可用getOrNullString(3)取回再用??兜底let ip result.getOrNullString(3) ?? NULLresult.getString(3)直取会抛NoneValueException回忆第 9 课。期望输出id 号随环境不同id11 ipNULL下节预告插入是把数据写进去下一件高频操作是把已有的数据改对或删掉。下一课更新与删除UPDATE / DELETE——学WHERE子句怎么精确命中目标行避免一改全表、一删全库的悲剧顺便补上UPDATE ... SET的占位符用法。系列说明本系列基于 Windows 平台 CIDE 仓颉 SDK1.2.0编写。本文全部代码、运行输出与报错文案均已在 SDK 1.2.0 MySQL 5.7 环境下逐块编译运行、逐字实测如遇 SDK 版本差异导致的细节出入以你本地版本为准欢迎评论区交流。 工具下载本系列全程使用的仓颉 IDE ——CIDE免费开源、社区版GitCode 仓库 / 安装包下载https://gitcode.com/wp_upala/cide打开页面后进入发行版Releases两种包任选其一安装版下载CIDE-版本-x64-Setup.exe双击安装适合日常长期使用免安装版Portable下载CIDE-版本-x64-Portable.zip解压到任意目录即用不写注册表、不留安装痕迹拷到 U 盘也能在别的电脑直接运行包内附《使用说明.txt》。适合先试用、或在受限电脑上学习本系列课程。仓颉 SDK 请前往仓颉编程语言官网下载https://cangjie-lang.cn