|
@@ -0,0 +1,444 @@
|
|
|
|
+package lxDb
|
|
|
|
+
|
|
|
|
+import (
|
|
|
|
+ "errors"
|
|
|
|
+ "fmt"
|
|
|
|
+ "git.listensoft.net/tool/lxutils/lxUtil"
|
|
|
|
+ "gorm.io/gorm"
|
|
|
|
+ "strings"
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+// 带事务的, 和不带事务的 说明:
|
|
|
|
+// 如果需要支持事务请调aaaTx方法, 并传开启事务的DB
|
|
|
|
+
|
|
|
|
+// OneById 通过ID查询一条
|
|
|
|
+func OneById(m interface{}, id uint) (err error) {
|
|
|
|
+ return OneByIdTx(DB, m, id)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// OneByIdTx 通过ID查询一条
|
|
|
|
+func OneByIdTx(tx *gorm.DB, m interface{}, id uint) (err error) {
|
|
|
|
+ if id == 0 {
|
|
|
|
+ return errors.New("主键ID不能为空")
|
|
|
|
+ }
|
|
|
|
+ if tx.Take(m, id).Error != nil { // or First ?
|
|
|
|
+ return errors.New("未找到记录")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// One 查询一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错.
|
|
|
|
+func One(m interface{}) (err error) {
|
|
|
|
+ return OneTx(DB, m)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// OneTx 查询一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错.
|
|
|
|
+func OneTx(tx *gorm.DB, m interface{}) error {
|
|
|
|
+ return oneTx(tx, m, "")
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func oneTx(tx *gorm.DB, m interface{}, Type string) (err error) {
|
|
|
|
+ //reflectVal := reflect.ValueOf(m)
|
|
|
|
+ //t := reflect.Indirect(reflectVal).Type()
|
|
|
|
+ //newObj := reflect.New(t)
|
|
|
|
+ if lxUtil.IsZeroOfUnderlyingType(m) {
|
|
|
|
+ return errors.New("条件不能为空")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 这里有一个特别的情况, 如果m.id有值, 生成sql的where里id条件出现两次, 但是不影响效果
|
|
|
|
+ if Type == "first" { // 第一个
|
|
|
|
+ err = tx.Where(m).First(m).Error
|
|
|
|
+ } else if Type == "last" { // 最后一个
|
|
|
|
+ err = tx.Where(m).Last(m).Error
|
|
|
|
+ } else { // 就是一个
|
|
|
|
+ err = tx.Where(m).Take(m).Error
|
|
|
|
+ }
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// First 查询第一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错.
|
|
|
|
+func First(m interface{}) (err error) {
|
|
|
|
+ return FirstTx(DB, m)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// FirstTx 查询第一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错.
|
|
|
|
+func FirstTx(tx *gorm.DB, m interface{}) error {
|
|
|
|
+ return oneTx(tx, m, "first")
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Last 查询最后一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错.
|
|
|
|
+func Last(m interface{}) (err error) {
|
|
|
|
+ return LastTx(DB, m)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// LastTx 查询最后一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错.
|
|
|
|
+func LastTx(tx *gorm.DB, m interface{}) error {
|
|
|
|
+ return oneTx(tx, m, "last")
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// One 查询一条. 这种方式时不行的, 实际上对应的表是 base_model
|
|
|
|
+//func (m *BaseModel) One() (err error) {
|
|
|
|
+// //if DB.Where(m).First(one).RecordNotFound() {
|
|
|
|
+// if DB.Where(m).First(m).Error != nil {
|
|
|
|
+// return errors.New("resource is not found")
|
|
|
|
+// }
|
|
|
|
+// return nil
|
|
|
|
+//}
|
|
|
|
+
|
|
|
|
+// Create 新增
|
|
|
|
+func Create(m interface{}) error {
|
|
|
|
+ return CreateTx(DB, m)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// CreateTx 带事务的, 新增
|
|
|
|
+func CreateTx(tx *gorm.DB, m interface{}) error {
|
|
|
|
+ return tx.Create(m).Error
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Update 更新一条数据的单个字段, m.ID必须有值
|
|
|
|
+func Update(m interface{}, field string, value interface{}) error {
|
|
|
|
+ return UpdateTx(DB, m, field, value)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// UpdateTx 带事务的, 更新一条数据的单个字段, m.ID必须有值
|
|
|
|
+func UpdateTx(tx *gorm.DB, m interface{}, field string, value interface{}) error {
|
|
|
|
+ db := tx.Model(m).Update(field, value)
|
|
|
|
+ if err := db.Error; err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ if db.RowsAffected != 1 {
|
|
|
|
+ return errors.New("id is invalid and resource is not found")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Updates 更新一条数据的多个字段, m.ID必须有值
|
|
|
|
+// FIXME: 只会更新非零值的字段!!
|
|
|
|
+// 尽量不要让m和fields是同一个对象, fields只赋值需要更新的字段
|
|
|
|
+// TODO 支持更新成零值, 支持结构体 map, 放在表结构体里? 不通过ID更新?
|
|
|
|
+// TODO: fields 里不能ID有值, 否则也更新ID, 这是不行的
|
|
|
|
+func Updates(m interface{}, fields interface{}) error {
|
|
|
|
+ return UpdatesTx(DB, m, fields)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// UpdatesTx 带事务的, 更新一条数据的多个字段, m.ID必须有值
|
|
|
|
+func UpdatesTx(tx *gorm.DB, m interface{}, fields interface{}) error {
|
|
|
|
+ db := tx.Model(m).Updates(fields)
|
|
|
|
+ if err := db.Error; err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ if db.RowsAffected != 1 {
|
|
|
|
+ return errors.New("id is invalid and resource is not found")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Delete 删除, m.ID必须有值.
|
|
|
|
+func Delete(m interface{}) error {
|
|
|
|
+ //func Delete(m interface{}, conds ...interface{}) error { // 不做这种支持, 由ma.DB.Delete()去支持
|
|
|
|
+ return DeleteTx(DB, m)
|
|
|
|
+ //return DeleteTx(nil, m, conds...)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// DeleteTx 删除, m.ID必须有值. tx不为空就是带事务的
|
|
|
|
+func DeleteTx(tx *gorm.DB, m interface{}) error {
|
|
|
|
+ //func DeleteTx(tx *gorm.DB, m interface{}, conds ...interface{}) error {
|
|
|
|
+ db := tx.Delete(m)
|
|
|
|
+ //db := tx.Delete(m, conds...)
|
|
|
|
+ if err := db.Error; err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ if db.RowsAffected != 1 {
|
|
|
|
+ return errors.New("未找到要删除的数据")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// List 查询数据列表, m是库表结构体, m的有值属性会作为查询条件, 且必须有条件, list里个体的类型可以与m的类型不同
|
|
|
|
+func List(m interface{}, list interface{}) (err error) {
|
|
|
|
+ return ListTx(DB, m, list)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// ListTx 查询数据列表, m是库表结构体, m的有值属性会作为查询条件, 且必须有条件, list里个体的类型可以与m的类型不同
|
|
|
|
+func ListTx(tx *gorm.DB, m interface{}, list interface{}) (err error) {
|
|
|
|
+ if lxUtil.IsZeroOfUnderlyingType(m) {
|
|
|
|
+ return errors.New("条件不能为空")
|
|
|
|
+ }
|
|
|
|
+ if tx.Model(m).Where(m).Find(list).Error != nil {
|
|
|
|
+ // if tx.Where(m).Find(list).Error != nil {
|
|
|
|
+ return errors.New("查询出现错误")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// ListAll 查询所有数据, m是库表结构体, m的有值属性不会作为查询条件, list里个体的类型可以与m的类型不同
|
|
|
|
+func ListAll(m interface{}, list interface{}) (err error) {
|
|
|
|
+ return ListAllTx(DB, m, list)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// ListAllTx 查询所有数据, m是库表结构体, m的有值属性不会作为查询条件, list里个体的类型可以与m的类型不同
|
|
|
|
+func ListAllTx(tx *gorm.DB, m interface{}, list interface{}) (err error) {
|
|
|
|
+ if tx.Model(m).Find(list).Error != nil {
|
|
|
|
+ // if tx.Where(m).Find(list).Error != nil {
|
|
|
|
+ return errors.New("查询出现错误")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Query 查询数据列表, 支持分页、总数、汇总, 见PaginationQuery属性. PaginationQuery不可为nil
|
|
|
|
+func Query(m interface{}, list interface{}, q *PaginationQuery) (err error) {
|
|
|
|
+ QueryTx(DB, m, list, q)
|
|
|
|
+ return
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// QueryTx 带事务的查询数据列表. tx为空就是不带事务, 否则认为是开启了事务. 你应该避免使用次方法, 而是使用Query
|
|
|
|
+func QueryTx(tx *gorm.DB, m interface{}, list interface{}, q *PaginationQuery) (err error) {
|
|
|
|
+ // !! 关于会话 新的Statement实例 及复用 ref: https://gorm.io/zh_CN/docs/method_chaining.html
|
|
|
|
+
|
|
|
|
+ if q == nil {
|
|
|
|
+ err = errors.New("paginationQuery不可为nil")
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ tx = tx.Model(m)
|
|
|
|
+
|
|
|
|
+ // 注意: count, 查询list, summary的顺序不能变.
|
|
|
|
+
|
|
|
|
+ tx = q.Build(tx) // 构造查询条件
|
|
|
|
+
|
|
|
|
+ // 记录条数
|
|
|
|
+ if needDoCount(q) {
|
|
|
|
+ var total int64
|
|
|
|
+ tx = tx.Count(&total)
|
|
|
|
+ q.Total = int(total)
|
|
|
|
+ if total == 0 { // 如果查了记录条数并且是0, 就不需要查记录和汇总了
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if q.OrderBy != "" {
|
|
|
|
+ tx = tx.Order(lxUtil.FieldToColumn(q.OrderBy)) // TODO: q.OrderBy是字符串,可能多个字段 会有问题吗
|
|
|
|
+ //tx = tx.Order(q.OrderBy)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if q.Offset > 0 {
|
|
|
|
+ tx = tx.Offset((q.Offset - 1) * q.Limit)
|
|
|
|
+ }
|
|
|
|
+ if q.Limit > 0 {
|
|
|
|
+ tx = tx.Limit(q.Limit)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 获取查询值
|
|
|
|
+ err = tx.Find(list).Error
|
|
|
|
+
|
|
|
|
+ // 获取汇总信息, 如果不需要查记录条数就不再查汇总
|
|
|
|
+ if needDoCount(q) {
|
|
|
|
+ if q.Summary != "" && len(q.SummarySql) == 0 {
|
|
|
|
+ q.SummarySql = fieldsToSumSql(q.Summary)
|
|
|
|
+ }
|
|
|
|
+ if len(q.Summary) != 0 {
|
|
|
|
+ tx = tx.Offset(-1) // 需要去除offset, 否则结果可能为空, 注意: 设置0不起作用.
|
|
|
|
+ var summary = make(map[string]interface{})
|
|
|
|
+ //tx.Order("") // FIXME: 怎么去掉order by, sum是不需要order by的, 影响性能.
|
|
|
|
+ tx.Select(q.SummarySql).Take(&summary)
|
|
|
|
+
|
|
|
|
+ // []byte 转 string. 不太合理, 应该返回int或float
|
|
|
|
+ for k, v := range summary {
|
|
|
|
+ if bs, ok := v.([]byte); ok {
|
|
|
|
+ summary[k] = string(bs)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ q.SummaryResult = summary
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+//// SqlOne 原生SQL查询一个
|
|
|
|
+//func SqlOne(sql string, m interface{}) {
|
|
|
|
+// DB.Raw(sql, m)
|
|
|
|
+//}
|
|
|
|
+//
|
|
|
|
+//// SqlList 原生SQL查询列表
|
|
|
|
+//func SqlList() {
|
|
|
|
+//
|
|
|
|
+//}
|
|
|
|
+
|
|
|
|
+// SqlQuery 原生SQL查询列表, 支持分页. PaginationQuery可为nil
|
|
|
|
+func SqlQuery(sql string, list interface{}, q *PaginationQuery, params ...interface{}) (err error) {
|
|
|
|
+ return SqlQueryTx(DB, sql, list, q, params...)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func SqlQueryTx(tx *gorm.DB, sql string, list interface{}, q *PaginationQuery, params ...interface{}) (err error) {
|
|
|
|
+ var builder strings.Builder
|
|
|
|
+ builder.WriteString(sql)
|
|
|
|
+
|
|
|
|
+ if params == nil {
|
|
|
|
+ params = make([]interface{}, 0)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 条件字段
|
|
|
|
+ if q != nil {
|
|
|
|
+ where, args := q.BuildRawWhere()
|
|
|
|
+ if hasWhere(sql) { // 原SQL已有WHERE子句
|
|
|
|
+ builder.WriteString(where) // 去掉where 前头的and or ..
|
|
|
|
+ } else { // 原SQL没有WHERE子句
|
|
|
|
+ if strings.HasPrefix(where, " AND ") {
|
|
|
|
+ where = strings.Replace(where, " AND ", " WHERE ", 1)
|
|
|
|
+ builder.WriteString(where)
|
|
|
|
+ } else if strings.HasPrefix(where, " OR ") {
|
|
|
|
+ where = strings.Replace(where, " OR ", " WHERE ", 1)
|
|
|
|
+ builder.WriteString(where)
|
|
|
|
+ } else {
|
|
|
|
+ builder.WriteString(where) // "" 或者 " GROUP BY ..."
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if len(args) > 0 {
|
|
|
|
+ params = append(params, args...)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 半成品 sql 用于查询其他信息
|
|
|
|
+ var sql2 = builder.String()
|
|
|
|
+
|
|
|
|
+ // 记录条数
|
|
|
|
+ if needDoCount(q) {
|
|
|
|
+ var total int64
|
|
|
|
+ //tx = tx.Count(&total)
|
|
|
|
+ tx.Raw("SELECT COUNT(*) as total FROM ("+sql2+") aaaa", params...).Take(&total)
|
|
|
|
+ q.Total = int(total)
|
|
|
|
+ if total == 0 { // 如果查了记录条数并且是0, 就不需要查记录和汇总了
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 获取汇总信息 // TODO: 汇总应该放到查询列表的后面
|
|
|
|
+ if q.Summary != "" && len(q.SummarySql) == 0 {
|
|
|
|
+ q.SummarySql = fieldsToSumSql(q.Summary)
|
|
|
|
+ }
|
|
|
|
+ if len(q.Summary) != 0 {
|
|
|
|
+ tx = tx.Offset(-1) // 需要去除offset, 否则结果可能为空, 注意: 设置0不起作用.
|
|
|
|
+ var summary = make(map[string]interface{})
|
|
|
|
+ //tx.Order("") // FIXME: 怎么去掉order by, sum是不需要order by的, 影响性能.
|
|
|
|
+ //tx.Select(q.SummarySql).Take(&summary) // 不适合rawsql?
|
|
|
|
+
|
|
|
|
+ tx.Raw("SELECT "+strings.Join(q.SummarySql, ", ")+" FROM ("+sql2+") ssss", params...).Take(&summary)
|
|
|
|
+
|
|
|
|
+ // []byte 转 string. 不太合理, 应该返回int或float
|
|
|
|
+ for k, v := range summary {
|
|
|
|
+ if bs, ok := v.([]byte); ok {
|
|
|
|
+ summary[k] = string(bs)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ q.SummaryResult = summary
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 排序处理
|
|
|
|
+ if q.OrderBy != "" {
|
|
|
|
+ s := fmt.Sprintf(" ORDER BY %s", lxUtil.FieldToColumn(q.OrderBy)) // TODO: q.OrderBy是字符串,可能多个字段 会有问题吗
|
|
|
|
+ builder.WriteString(s)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 偏移量处理
|
|
|
|
+ if q.Limit > 0 {
|
|
|
|
+ if q.Offset > 0 {
|
|
|
|
+ offset := (q.Offset - 1) * q.Limit
|
|
|
|
+ s := fmt.Sprintf(" LIMIT %d, %d", offset, q.Limit)
|
|
|
|
+ builder.WriteString(s)
|
|
|
|
+ } else {
|
|
|
|
+ s := fmt.Sprintf(" LIMIT %d", q.Limit)
|
|
|
|
+ builder.WriteString(s)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //tx.Raw(builder.String(), params...).Scan(list) // FIXME: unsupported data type: &[] why?
|
|
|
|
+ tx.Raw(builder.String(), params...).Find(list) // Find与Scan区别: list传入[]时, 查询为空的情况下, Find返回的是[], 而Scan返回的是nil.
|
|
|
|
+ // ref: What is the difference between Find and Scan: https://github.com/go-gorm/gorm/issues/4218
|
|
|
|
+
|
|
|
|
+ return
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 是否需要查询记录条数
|
|
|
|
+func needDoCount(q *PaginationQuery) bool {
|
|
|
|
+ if q.NoTotal {
|
|
|
|
+ return false
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if q.Limit == 0 { // 不限制条数, 等同于查所有记录, 这时候就不需要查记录条数
|
|
|
|
+ return false
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //return q.Offset <= 1 // todo lcs 为什么要这样写 第二页都没了
|
|
|
|
+ return true
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// utils -----------------
|
|
|
|
+
|
|
|
|
+// "inAmt,inCount" -> ["SUM(int_amt) AS inAmt", "SUM(int_count) AS inCount"]
|
|
|
|
+func fieldsToSumSql(fields string) (sumSqls []string) {
|
|
|
|
+ strs := strings.Split(strings.TrimSpace(fields), ",")
|
|
|
|
+ for _, str := range strs {
|
|
|
|
+ field := strings.TrimSpace(str)
|
|
|
|
+ if field != "" {
|
|
|
|
+ sumSqls = append(sumSqls, "SUM("+lxUtil.FieldToColumn(field)+") AS "+field+"")
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// SELECT...FROM...[WHERE] 句式的 SQL 中是否存在 WHERE 子句
|
|
|
|
+func hasWhere(sql string) bool {
|
|
|
|
+ deep := 0 // 括号嵌套层数
|
|
|
|
+ step := 0 // "where" 匹配进度
|
|
|
|
+
|
|
|
|
+ // 遍历 sql 忽略 ' ( ` 判断是否存在 where
|
|
|
|
+ for i := 0; i < len(sql); i++ {
|
|
|
|
+ switch sql[i] {
|
|
|
|
+ case '(':
|
|
|
|
+ deep++
|
|
|
|
+ case ')':
|
|
|
|
+ deep--
|
|
|
|
+ case 96: // "`"
|
|
|
|
+ // 下一个 "`" 的下标
|
|
|
|
+ // 忽略其他字符
|
|
|
|
+ for i = i + 1; i < len(sql); i++ {
|
|
|
|
+ if sql[i] == 96 {
|
|
|
|
+ break
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ case 39: // "'"
|
|
|
|
+ // 下一个 "'" 的下标
|
|
|
|
+ // 忽略其他字符
|
|
|
|
+ for i = i + 1; i < len(sql); i++ {
|
|
|
|
+ if sql[i] == 39 {
|
|
|
|
+ break
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ default:
|
|
|
|
+ if deep != 0 {
|
|
|
|
+ continue
|
|
|
|
+ }
|
|
|
|
+ if step == 5 {
|
|
|
|
+ return true
|
|
|
|
+ }
|
|
|
|
+ if sql[i] == where[step][0] || sql[i] == where[step][1] {
|
|
|
|
+ step++
|
|
|
|
+ } else {
|
|
|
|
+ step = 0
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return false
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+var where = []string{
|
|
|
|
+ "Ww",
|
|
|
|
+ "Hh",
|
|
|
|
+ "Ee",
|
|
|
|
+ "Rr",
|
|
|
|
+ "Ee",
|
|
|
|
+}
|