Sequelize手记 - (一)
最近开始接触数据库,现在普遍用的都是Mysql
数据库,简单的了解了一下sql
语句,没有太深入的学习,然后就开始找相关的ORM
框架,然后锁定了Sequelize
,个人感觉很强大,搜索了一些文档,但是很让人费解,讲的每一部分都是那么的官方,不太容易理解,记录一下学习路程。本文档以koa
+Sequelize
进行编码测试。
准备工作
在尝试使用Sequelize
之前先确认是否安装了Mysql
数据库,安装node
,这里使用的Mysql 5.6
版本。
首先要创建一个项目执行命令如下:
mkdir 文件夹名称
cd 文件夹名称
npm init -y // 直接略过所有问答,全部采用默认答案
在开始之前首先要安装相关依赖:
// 安装 sequelize koa mysql2
npm install --save-dev sequelize koa mysql2
注意:这里是mysql2
建立连接
创建main.js
文件,分别引入koa
和sequelize
建立与mqsql
数据库连接。在实例化Sequelize
时需要配置一些参数,第一个接收的参数时数据库的名称,第二个是用户名,第三个是密码,第四项是连接mysql
的相关配置。
const Koa = require("koa");
const Sequelize = require("sequelize");
const app = new Koa();
const mysqlConfig ={
host: 'localhost', // 接数据库的主机
port: '3306', // 接数据库的端口
protocol: 'tcp', // 连接数据库使用的协议
dialect: 'mysql', // 使用mysql
pool: {
max: 5, // 最大连接数量
min: 0, // 最小连接数量
idle: 10000 // 连接空置时间(毫秒),超时后将释放连接
},
retry: { // 设置自动查询时的重试标志
max: 3 // 设置重试次数
},
omitNull: false, // null 是否通过SQL语句查询
timezone: '+08:00' // 解决时差 - 默认存储时间存在8小时误差
};
const sequelize = new Sequelize('aarontest', 'root', '123456',mysqlConfig );
app.listen(3000);
通过上述代码就已经与Mysql
建立了连接。这个地方没有什么可以讲的,按照上述进行配置就可以了。test
数据库不一定要存在不存在也是可以正常建立连接的。配置参数还有很多,这里只说了一些常用的,就不多赘述了。如果觉得上述方法比较麻烦可以通过也提供了其他方法进行连接:
const sequelize = new Sequelize('mysql://root:123456@localhost:3306/aarontest', {
...mysqlConfig
});
这种连接方式与mongoose
连接方法差不多,以至于具体使用哪种方式还是要根据个人的编码习惯来决定。
建立model
sequelize
是通过define
方法建立模型的,Model
相当于数据库中的表,该对象不能通过构造函数实例化,而只能通过sequelize.define()
或sequelize.import()
方法创建。
const AaronTest = sequelize.define('project', {
title: Sequelize.STRING,
description: Sequelize.TEXT
})
通过上面的方法创建好model
之后,使用Navicat
(数据库可视化工具)仍然无法看到我们所创建的表,尴尬。。。sequelize
提供了一个sync()
方法可以同步模型到数据库。使用该方法的时候一定要注意所连接的数据库一定要存在
否则会报错。
define
方法接收三个参数,第一个参数为表名称,第二个为所需要创建的数据库字段,第三个参数是相关表配置。
const AaronTest = sequelize.define('project', {
title: Sequelize.STRING,
description: Sequelize.TEXT
});
AaronTest.sync();
这样model
所创建的字段就被同步到了数据库中,同样相对应的表也被创建好了,需要注意通过这种方式同步的表会在表名称后面添加一个s
作为复数。同步数据库是会默认添加两个字段createdAt
和updatedAt
有的时候可能不需要这两个字段。这个时候需要在第三个字段中添加timestamps
为false
默认为true
。
const AaronTest = sequelize.define('project', {
title: Sequelize.STRING,
description: Sequelize.TEXT
},{
timestamps: false
})
上面可以看出通过Sequelize.STRING
设置当前字段的类型,Sequelize
提供了很多数据类型供我们进行使用:
类型 | 说明 |
---|---|
STRING | 将字段指定为变长字符串类型,默认长度为 255。Sequelize.STRING(64) |
CHAR | 将字段指定为定长字符串类型,默认长度为 255。Sequelize.CHAR(64) |
TEXT | 将字段指定为(无)有限长度的文本列。可用长度:tiny, medium, long,Sequelize.TEXT('tiny') |
INTEGER | 32位整型,可用属性:UNSIGNED,ZEROFILL,Sequelize.INTEGER('UNSIGNED') |
BOOLEAN | 小数,接受一个或两个参数表示精度,Sequelize.BOOLEAN() |
TIME | 指定为时间类型列,Sequelize.TIME() |
DATE | 指定为日期时间类型列,Sequelize.DATE() |
DATEONLY | 指定为日期类型列,Sequelize.DATEONLY() |
HSTORE | 指定为键/值类型列,仅Postgres适用,Sequelize.HSTORE() |
JSON | 指定为JSON字符串类型列,仅Postgres适用,Sequelize.JSON() |
JSONB | 指定为预处理的JSON数据列,仅Postgres适用,Sequelize.JSONB() |
NOW | 一个表示当前时间戳的默认值,Sequelize.NOW() |
UUID | UUID类型列,其默认值可以为UUIDV1或UUIDV4,Sequelize.UUID() |
ENUM | 枚举类型,Sequelize.ENUM() |
ARRAY | 数组类型,仅Postgres适用,Sequelize.ARRAY() |
设定model的时候在添加第三个参数可以对当前model
进行二次设置,以使数据库更加强壮,常用字段如下
- timestamps:不要添加时间戳属性 (updatedAt, createdAt)
- paranoid:paranoid 属性只在启用 timestamps 时适用,不从数据库中删除数据,而只是增加一个 deletedAt 标识当前时间,我们常说的逻辑删除
- underscored: 不使用驼峰式命令规则,这样会在使用下划线分隔,updatedAt的字段名会是 updated_at
- freezeTableName:禁止修改表名. 默认情况下sequelize会自动使用传入的模型名(define的第一个参数)做为表名,如果你不想使用这种方式你需要进行以下设置
- tableName:定义表名
- createdAt:不想使用 createdAt
- updatedAt:想 updatedAt 的实际名为'***'
- deletedAt: 要将 deletedAt 设置为 destroyTime (注意要启用paranoid)
当创建model
的时候可能需要有些附加属性,比如主键
,自增
,不能为null
,默认值等等,可以在创建model
的时候进行手动设置。
- autoIncrement:是否自增
- references:通过references选项可以创建外键
- allowNull:设置 allowNull 选项为 false 后,会为列添加 NOT NULL 非空限制
- defaultValue:设置默认值
- type:字段类型
- unique:添加唯一(unique)约束后插入重复值会报错,unique属性可以是boolean 或 string类型
- primaryKey:设置为主键
- comment:字段描述
- field:指定数据库中的字段名
除了这些以外Sequelize
在定义model
的时候,还提供了一个validate
属性,可以为添加的字段进行校验处理:
字段 | 说明 | 值类型 |
---|---|---|
is | 存储值必须满足正则 | 正则 |
not | 除正则之外的值 | 布尔 |
isEmail | 是否为邮箱 | 布尔 |
isUrl | 检查Url格式 | 布尔 |
isIP | 检查 IPv4 或 IPv6 格式 | 布尔 |
isIPv4 | 检查 IPv4 | 布尔 |
isIPv6 | 检查 IPv6 | 布尔 |
isAlpha | 不能使用字母 | 布尔 |
isAlphanumeric | 只允许字母数字字符 | 布尔 |
isNumeric | 只能使用数字 | 布尔 |
isInt | 只能是整数 | 布尔 |
isFloat | 只能是浮点数 | 布尔 |
isDecimal | 检查数字 | 布尔 |
isLowercase | 检查小写字母 | 布尔 |
isUppercase | 检查大写字母 | 布尔 |
notNull | 不允许null | 布尔 |
isNull | 只能为null | 布尔 |
notEmpty | 不能空字符串 | 布尔 |
equals | 只能使用指定值 | 字符串 |
contains | 必须包含子字符串 | 字符串 |
notIn | 不能是数组中的任意一个值 | 数组 |
isIn | 只能是数组中的任意一个值 | 数组 |
notContains | 不能包含子字符串 | 字符串 |
len | 值的长度必在 2 和 10 之间 | 数组 |
isUUID | 只能是UUID | 数字 |
isDate | 只能是日期字符串 | 布尔 |
isAfter | 只能使用指定日期之后的时间 | 字符串 |
isBefore: | 只能使用指定日期之前的时间 | 字符串 |
max | 允许的最大值 | 数字 |
min | 允许的最小值 | 数字 |
isArray | 不能使用数组 | 布尔 |
isCreditCard | 检查是有效的信用卡 | 布尔 |
除了上面的校验方法以外还提供了自定义校验方法,使用isEven
去定义一个函数,其函数的第一个参数就是所存如的值:
const AaronTest = sequelize.define('project', {
title: Sequelize.STRING,
description: {
type:Sequelize.TEXT,
validate:{
isEven: function(value) {
if(parseInt(value) % 2 != 0) {
throw new Error('Only even values are allowed!')
}
}
}
}
},{
timestamps: false
})
上面说过使用sequelize.import()
也可以创建model
这个方法其实是模型导入,通过文件导入模型定义。检查模型是否已经定义。被导入的模型会被缓存,所以多次导入并不会重复加载,path表示要导入文件的路径,如果使用相对路径会自动转换为绝对路径。
const AaronTest = sequelize.import('../model/user.js');
user.js
module.exports = function(sequelize, DataTypes) {
return sequelize.define("project", {
name: DataTypes.STRING,
description: DataTypes.TEXT
})
}
CRUD
CRUD:是指在做计算处理时的增加(Create)、读取(Read)、更新(Update)和删除(Delete)几个单词的首字母简写。crud主要被用在描述软件系统中数据库或者持久层的基本操作功能。
创建
创建数据的方法有很多种,这里简单的介绍一些常用的:
第一种:
先创建数据实例,然后调用实例的save
方法,完成数据存储。
const Aaron = AaronTest.build({
'title': `后端 | ${Math.random()}`,
'description': '技术部'
});
Aaron.save().then((result) => {
// 成功
console.log(result)
}).catch((error) => {
// 失败
console.log(error)
})
第二种:
通过静态create
方法
const user = AaronTest.create({
'title': `前端 | ${Math.random()}`,
'description': '网络部'
}).then(function(result) {
// 成功
console.log(result)
}).catch(function(error) {
// 失败
console.log(error)
});
读取
通过findAll
方法读取数据。
AaronTest.findAll({
where: {
description: '网络部'
},
limit: 10,
offset: 0
}).then(function(result) {
// success
console.log(result)
}).catch(function(error) {
// error
console.log(error)
});
通过上述方法创建的数据可以直接作为返回结果返回给前台,但是如果对查询到的结果进行数据操作时不行的,因为查询到的结果是sequelize
处理过的模型,如果想对其操作需要在参数中添加row:true
属性。
AaronTest.findAll({
where: {
description: '网络部'
},
limit: 10, // 查询多少条
offset: 0, // 查询开始位置
raw:true
}).then(function(result) {
// success
console.log(result)
}).catch(function(error) {
// error
console.log(error)
});
添加row:true
返回的则是一个没有被包装过的数组了。在项目过程中需要查询一下当前所查询的数据共有多少条返回给前端。
AaronTest.count({
where:{
description: '网络部'
}
}).then().then(function(result) {
// success
console.log(result)
}).catch(function(error) {
// error
console.log(error)
});
使用这种方法是确实可以查询到想要的结果,但是无论是先查询列表还是先查询总数,都需要对数据库进行两次查询很繁琐。sequelize
提供了一个很方便的方法。
AaronTest.findAndCountAll({
where: {
description: '网络部'
},
limit: 10,
offset: 0,
raw:true,
attributes:["id", "title"] // 需要查询出的字段
}).then(function(result) {
// success
console.log(result)
}).catch(function(error) {
// error
console.log(error)
});
通过上面的方法则可以查询到总数以及条件范围内的数据,一举两得。查询结果返回的是一个json
对象,其包括cont
和rows
两个属性,分别是总数和数据。很舒服有没有。
以上方式是查询列表,查询单条数据使用其他方法:
AaronTest.findOne({
where:{
id:6
},
raw:true,
attributes:["id", "title"]
}).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
更新
修改数据可以直接调用静态的update
方法,通过where
条件查询,对其搜索到的数据进行查询,并对查询到的数据进行更改。
AaronTest.update({
description: '前端部',
title:`前端 | ${Math.random()}`
},{
where:{
description: "网络部"
}
}).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
该方法修改所有查询的到的数据,返回结果为数组形式,数据只有一个值,也就是数组的第0
项,则是N
条数据修改成功。
删除
删除操作通过destroy
方法,同样也是通过where
条件查询,对所查询数据进行删除。
AaronTest.destroy({
where: {
description: "UI部",
}
}).then(function(result) {
console.log(result)
}).catch(function(error) {
console.log(error)
});
当删除成功后,返回结果为Number
,删除多少条数据,如果没有删除则会返回0
。此方法属于物理删除,删除后无法进行恢复。
查询参数
CRUD
操作过程中,都少不了的就是查询,细心的应该可以看的出,上面的例子中查询的时候多多少少的对其进行了一些小的改动。Sequelize
中有两种查询:使用Model
(模型)中的方法查询和使用sequelize.query()
进行基于SQL
语句的原始查询。上面用到的是Model
查询方式,接下来就详细的介绍一些常用的参数以及其代表的意义。
attributes - 属性与查询字段
查询时,如果只需要查询模型的部分属性,可以在通过在查询选项中指定attributes
实现。该选项是一个数组参数,在数组中指定要查询的属性即可,这个字段在上面进行查询的时候已经使用过了。
AaronTest.findOne({
where:{
id:6
},
raw:true,
attributes:["id", "title", "description"]
}).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
查询属性(字段)可以通过传入一个嵌套数据进行重命名,这里需要强调一下重命名所指的是对查询出的数据键值进行重命名处理,而不是更改数据表中的字段名称。
AaronTest.findOne({
where:{
id:2
},
attributes:["id", ["title","t"]],
raw:true
}).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
// 注意这里t ↓
// { id: 2, t: '前端 | 0.8765218593370694' }
通过sequelize.fn
方法可以进行聚合查询,个人觉得这个方法不太常用,但是还是简单的介绍一下,这种查询方式是向当前查询内容中添加一个新的属性,并且查询的是列表还是查询单条数据。
应用场景:
比如有很多商品,每个商品都有自己的分类,根据id
进行查询了一个商品,但是与其同类的商品有多少?就可以使用这个方法添加进去。下面例子中的count
则是添加进去属性的键。
AaronTest.findAll({
where:{
id:2
},
attributes: [
"id",
["title","t"],
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
],
raw:true
}).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
可能有一种情况,当前所需要查询的表字段太多,但是只有一两个数据不想要,在attributes
数组中添加很长的字段名称,这样会显得代码很臃肿。attributes
不光可以为数组,还可以为对象在对象存在exclude
这个属性,这个属性就是剔除掉那些不想要的属性。
AaronTest.findOne({
where:{
id:2
},
attributes:{
exclude: ['id']
},
raw:true
}).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
where - 指定筛选条件
上面的那么多例子中where
出现的次数最多了,除了增加数据不需要,其他的都需要用到where
条件,可以指定一个where
选项以指定筛选条件,where
是一个包含属性/值对对象,sequelize
会根据此对象生产查询语句的筛选条件。
where
的基础用法也就向上面那样,针对某些特定的条件进行查询处理。
AaronTest.findOne({
where:{
id:2
},
attributes:{
exclude: ['id']
},
raw:true
}).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
就像上面那样简单的查询无法满足所有的业务需求,Sequelize
还提供了操作符以满足更多的查询条件,常用的操作符如下:
$and: {a: 5} // AND (a = 5)
$or: [{a: 5}, {a: 6}] // (a = 5 OR a = 6)
$gt: 6, // > 6
$gte: 6, // >= 6
$lt: 10, // < 10
$lte: 10, // <= 10
$ne: 20, // != 20
$not: true, // IS NOT TRUE
$between: [6, 10], // BETWEEN 6 AND 10
$notBetween: [11, 15], // NOT BETWEEN 11 AND 15
$in: [1, 2], // IN [1, 2]
$notIn: [1, 2], // NOT IN [1, 2]
$like: '%hat', // LIKE '%hat'
$notLike: '%hat' // NOT LIKE '%hat'
$iLike: '%hat' // 包含'%hat' (case insensitive) (PG only)
$notILike: '%hat' // 不包含'%hat' (PG only)
$like: { $any: ['cat', 'hat']} // 像任何数组['cat', 'hat'] -也适用于iLike和notLike
limit/offset - 分页与限制返回结果数
在进行列表查询时,不能把查询道德所有数据全部返回出去,需要对数据进行分页处理。
// 获取 10 条数据(实例)
AaronTest.findAll({ limit: 10 })
// 跳过 8 条数据(实例)
AaronTest.findAll({ offset: 8 })
// 跳过 5 条数据并获取其后的 5 条数据(实例)
AaronTest.findAll({ offset: 5, limit: 5 })
查询排序
order
选项用于查询结果的排序数据。排序时应该传入一个包含属性-排序方向的元组/数组,以保证正确的转义:
AaronTest.findAll({
order: [
// 转义 username 并对查询结果按 DESC 方向排序
['username', 'DESC'],
// 按 max(age) 排序
sequelize.fn('max', sequelize.col('age')),
// 按 max(age) DESC 排序
[sequelize.fn('max', sequelize.col('age')), 'DESC'],
// 按 otherfunction(`col1`, 12, 'lalala') DESC 排序
[sequelize.fn('otherfunction', sequelize.col('col1'), 12, 'lalala'), 'DESC'],
// 按相关联的User 模型的 name 属性排序
[User, 'name', 'DESC'],
// 按相关联的User 模型的 name 属性排序并将模型起别名为 Friend
[{model: User, as: 'Friend'}, 'name', 'DESC'],
// 按相关联的User 模型的嵌套关联的 Company 模型的 name 属性排序
[User, Company, 'name', 'DESC'],
]
// 以下所有声明方式都会视为字面量,应该小心使用
order: 'convert(user_name using gbk)'
order: 'username DESC'
order: sequelize.literal('convert(user_name using gbk)')
})
上面说的这些对于SQL
语句了解一些,都是很容理解,有些API
不常用也就没些,详细可以查看中文文档。
SQL语句查询
原始查询中有两种替换查询参数的方法,以:开头的参数的形式替换或以不命名以?替换。在选项对象中传递参数:
- 如果传递一个数组,? 会按数组的顺序被依次替换
- 巢传递一个对象,:key将会用对象的键替换。如果对象中未找到指定键,则会引发异常(反之亦然)
// 这里是sequelize,并不是model
sequelize.query('SELECT * FROM projects WHERE id = ?',
{ replacements: ['active'], type: sequelize.QueryTypes.SELECT }
).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
总结
以上是对sequelize
的api
进行了整理,虽然不太全面,熟练掌握上述API
可以做一个项目了,有关sequelize
的更多的用法我也在继续爬坑中,可能文章中有些许错误,大家可以在下方留言,我会尽快做出改正。感谢大家的阅读。
Sequelize手记 - (一)的更多相关文章
- nodejs项目mysql使用sequelize支持存储emoji
nodejs项目mysql使用sequelize支持存储emoji 本篇主要记录nodejs项目阿里云mysql如何支持存储emoji表情. 因由 最近项目遇到用户在文本输入emoji进行存储的时候导 ...
- Linux.NET实战手记—自己动手改泥鳅(上)
各位读者大家好,不知各位读者有否阅读在下的前一个系列<Linux.NET 学习手记>,在前一个系列中,我们从Linux中Mono的编译安装开始,到Jexus服务器的介绍,以及如何在Linu ...
- Linux.NET学习手记(7)
前一篇中,我们简单的讲述了下如何在Linux.NET中部署第一个ASP.NET MVC 5.0的程序.而目前微软已经提出OWIN并致力于发展VNext,接下来系列中,我们将会向OWIN方向转战. 早在 ...
- Linux.NET学习手记(8)
上一回合中,我们讲解了Linux.NET面对OWIN需要做出的准备,以及介绍了如何将两个支持OWIN协议的框架:SignalR以及NancyFX以OwinHost的方式部署到Linux.NET当中.这 ...
- 关于《Linux.NET学习手记(8)》的补充说明
早前的一两天<Linux.NET学习手记(8)>发布了,这一篇主要是讲述OWIN框架与OwinHost之间如何根据OWIN协议进行通信构成一套完整的系统.文中我们还直接学习如何直接操作OW ...
- U3D DrawCall优化手记
在最近,使用U3D开发的游戏核心部分功能即将完成,中间由于各种历史原因,导致项目存在比较大的问题,这些问题在最后,恐怕只能通过一次彻底的重构来解决 现在的游戏跑起来会有接近130-170个左右的Dra ...
- 在node.js中,使用基于ORM架构的Sequelize,操作mysql数据库之增删改查
Sequelize是一个基于promise的关系型数据库ORM框架,这个库完全采用JavaScript开发并且能够用在Node.JS环境中,易于使用,支持多SQL方言(dialect),.它当前支持M ...
- 信息系统实践手记5-CACHE设计一例
说明:信息系统实践手记系列是系笔者在平时研发中先后遇到的大小的问题,也许朴实和细微,但往往却是经常遇到的问题.笔者对其中比较典型的加以收集,描述,归纳和分享. 摘要:此文描述了笔者接触过的部分信息系统 ...
- 信息系统实践手记6-JS调用Flex的性能问题一例
说明:信息系统实践手记系列是系笔者在平时研发中先后遇到的大小的问题,也许朴实和细微,但往往却是经常遇到的问题.笔者对其中比较典型的加以收集,描述,归纳和分享. 摘要:此文描述了笔者接触过的部分信息系统 ...
随机推荐
- servlet是什么?servlet到底是啥?
#说实话 这个鬼servlet我听说过它好多年了,但是我真的不知道它到底是干啥用的.内心里总觉得这是个很复杂的,绝对是让人难以理解的东西,我真的感觉自己很抗拒它,不想知道,不想去了解.可是我还是不得不 ...
- regexp_replace
pandas和SQL数据分析实战 https://study.163.com/course/courseMain.htm?courseId=1006383008&share=2&sha ...
- 【C++】C++中的动态内存解析
目录结构: contents structure [-] 动态内存和智能指针 使用shared_ptr管理内存 使用new直接管理内存 shared_ptr和new结合使用 unique_ptr we ...
- anaconda 安装指定源的包
当前知道的有两种方式可以查找: 1.conda search 命令:conda search pkg_name 安装的时候直接:conda install cudatoolkit=版本后 conda ...
- 【SpringBoot】SpringBoot 国际化(七)
本周介绍SpringBoot项目的国际化是如何处理的,阅读本章前请阅读[SpringBoot]SpringBoot与Thymeleaf模版(六)的相关内容 国际化原理 1.在Spring中有国际化Lo ...
- SpringCloud学习笔记-Eureka基础
Spring Cloud Eureka是Spring Cloud Netflix微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的微服务治理功能. 服务端 ...
- 使用filebeat发送nginx日志到kafka
1.配置filebeat_nginx.yml filebeat.modules: - module: nginx access: enabled: true var.paths: ["/va ...
- 【C/C++开发】C++11:右值引用和转发型引用
右值引用 为了解决移动语义及完美转发问题,C++11标准引入了右值引用(rvalue reference)这一重要的新概念.右值引用采用T&&这一语法形式,比传统的引用T&(如 ...
- Linux学习、Mongodb部署 踩到的坑学习
一.安装Centos 7虚拟机系统 1.系统安装 下载阿里云的镜像,下载后安装,默认全程图形界面:虚拟机使用Win10自带的Hyper:碰到的坑记录下 1.在Hyper加载镜像启动的时候,提示“虚拟机 ...
- Linux 下面 oracle 数据库连接工具的安装还有特殊字符密码登录的设置
1. 下载Oracle的连接客户端 https://www.oracle.com/database/technologies/instant-client/downloads.html 2. 我这里仅 ...