orm

package module
v1.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 26, 2021 License: MIT Imports: 20 Imported by: 1

README

1、封装数据库的操作,有orm的影子,但是又不是强orm,sql的查询鼓励单表操作,这个工具能够满足日常的表操作
insert/replace into [table] [value] [duplicate]
update [table] [value] [where]
delete [table] [where]
select [fields] from [table] [where] [group by] [having] [order by]

mysql.go 实现上诉语句的拆解封装,通过调用函数生成代码片段,最后执行的时候拼接成完整的sql语句执行,同时将要绑定的变量放到marks当中
允许通过Reset/Clear等方法清理重置sql片段   具体参考测试代码片段
func TestMysql(t *testing.T) {
	sqlSt := NewMysql()
	sql := sqlSt.Clear().Table("test").Where("id", 1).AsSql("select")
	t.Log(sql)

	sql = sqlSt.Clear().Table("test").Value("id", 1).Value("name", "leicc").AsSql("insert")
	t.Log(sql)

	sql = sqlSt.Clear().Table("test").Value("name", "demo").Where("id", 1, OP_EQ).AsSql("update")
	t.Log(sql)

	sqlSt.Clear().Table("test", "a").Field("a.id,b.refid").Table("user", "b", "a.id=b.refid")
	sql = sqlSt.Where("a.name", "demo").GroupBy("a.refid").OrderBy("a.id", DESC).AsSql("select")
	t.Log(sql)
}

query.go 主要是集成mysqlST,增加数据据句柄达到查询句柄的目的
Insert/Update/Delete/GetRow/GetList/GetItem/GetMap/NameMap/GetColumn/GetAsSql/GetValue
返回的数据类型依赖注入的field字段做映射,完成获取数据之后自动转换
func TestQuery(t *testing.T) {
	db, err := sqlx.Open("mysql", "root:@tcp(127.0.0.1:3306)/admin?charset=utf8mb4")
	if err != nil {
		t.Error(err)
		return
	}
	fields := map[string]reflect.Kind{
		"id":			reflect.Int64,	//账号id
		"openid":		reflect.String,	//第三方Openid
		"account":		reflect.String,	//登录账号
		"avatar":		reflect.String,	//用户的头像信息
		"loginpw":		reflect.String,	//登录密码 要求客户端md5之后传到服务端做二次校验
		"sex":			reflect.Int8,	//性别 1-男 2-女
		"nickname":		reflect.String,	//昵称
	}
	query := NewQuery(fields).SetDb(db)
	query.Table("sys_user").Where("id", 1, OP_EQ).Field("id,account,nickname")
	data := query.GetRow()
	t.Log(data)
	user := struct {
		Id 	int64 `json:"id"`
		Account string `json:"account"`
		NickName string `json:"nickname"`
	}{}
	err = query.GetRow().ToStruct(&user)
	t.Log(err, user)
	query.Clear().Table("sys_user").Field("id,account,nickname")
	list := query.GetList("", 0 , 2)
	t.Log(list)
	query.Clear().Table("sys_user").Field("id").Where("id", []int64{1,2,3,4}, OP_IN)
	column := query.GetColumn("", 0, -1)
	t.Log(column)
	query.Clear().Table("sys_user").Field("id as `key`, account as `val`").Where("id", []int64{1,2,3,4}, OP_IN)
	asmap := query.GetMap("", 0, -1)
	t.Log(asmap)
	query.Clear().Table("sys_user").Field("id,account,nickname").Where("id", []int64{1,2,3,4}, OP_IN)
	nsmap := query.NameMap("", "id", 0, -1)
	t.Log(nsmap)
}

model.go主要集成query+数据库连接池+缓存策略+数据表模型 实现表记录的增加、删除、修改、查询(取列表、取指定的列、取map结构等等)
缓存策略使用表级别的缓存版本号,一个表维护一个版本号,只要这个表有update/inster/delete 操作就更新版本号,这样这个表的所有数据都会自动失效。
例如GetList 操作缓存key user@hash(查询条件+版本号) 只要版本号变动,下一次取数据的时候缓存就无法命中,就可以取db,然后重新建立缓存,达到数据对数据的保护效果
model配置主/从数据库的获取配置的key信息,update/inster/delete 主库操作,select默认都是从库操作的。

通过自动化脚本生成orm
示例代码
package main

import (
	"gitee.com/leicc/go-orm"
	"gitee.com/leicc/go-orm/cache"
)

func main() {
	cacheSt := cache.CacheConfigSt{"redis", "redis://:@127.0.0.1:6379/1"}
	dbmaster := orm.DbConfig{"mysql", "root:@tcp(127.0.0.1:3306)/admin?charset=utf8mb4", 32, 32}
	config := struct {
		Redis  string
		Cache  cache.CacheConfigSt
		DbMaster orm.DbConfig
		DbSlaver orm.DbConfig
	}{"redis://:@127.0.0.1:6379/1", cacheSt, dbmaster, dbmaster}
	orm.LoadDbConfig(config)//配置数据库结构注册到数据库调用配置当中
	orm.CreateOrmModels("dbmaster", "dbslaver", "./models")
}

数据库映射到代码的工具,将每个表生成model,放到指定的目录,提供给项目使用,配置Redis的话将会使用Redis作为缓存策略

2、每个表继承orm.modelSt,也就集成了这个结构体的方法,包含了插入、删除、修改、取列表等等操作,查询的话自动会缓存数据,更新的时候清理缓存数据,通过每个表维护一个版本号更新.

/****************************************************************************************
	在这个类是动态生成,
 */
type demoUser struct {
	*ModelSt
}

//这里的dbPool
func newDemoUser() *demoUser {
	fields := map[string]reflect.Kind{
		"id":		reflect.Int64,	//账号id
		"openid":	reflect.String,	//第三方Openid
		"account":	reflect.String,	//登录账号
		...
		"stime":	reflect.Int,	//最后操作时间
	}

	args  := map[string]interface{}{
		"table":		"sys_user",
		"orgtable":		"sys_user",
		"prikey":		"id",
		"dbmaster":		"dbmaster",
		"dbslaver":		"dbslaver",
		"slot":			0,
	}

	data := &demoUser{&orm.ModelSt{}}
	data.Init(&orm.GdbPoolSt, args, fields)
	return data
}

4、根据id主键获取一条记录,默认返回SqlMap结构,可以加ToStruct转为结构体
	sorm := models.NewSysSyslog(dbCtx).SetYmTable("200601")
	data := struct {
		Id int `json:"id"`
		Ip string `json:"ip"`
		Msg string `json:"msg"`
		Stime int64 `json:"stime"`
	}{}
	err1 := sorm.GetOne(3).ToStruct(&data)
	fmt.Println(data, err1)

5、根据id主键更新记录	两种方式,直接使用SqlMap更新或者使用匿名函数设置要更新的字段
	sorm.Save(3, orm.SqlMap{"msg":"leicc"})
	sorm.SaveFromHandler(3, func(st *orm.QuerySt) *orm.QuerySt {
		st.Value("ip", "129.65.23.123")
		return st
	})
	err2 := sorm.GetOne(3).ToStruct(&data)
	fmt.Println(data, err2)
6、根据条件获取满足条件的某一条记录,同样使用匿名函数设置查询条件
	err3 := sorm.GetItem(func(st *orm.QuerySt) string {
		st.Where("id", 3)
		return st.GetWheres()
	}, "id,msg,ip,stime").ToStruct(&data)
	fmt.Println(data, "=========", err3)
7、根据获取获取列表,返回SqlMap切片列表
	list := sorm.GetList(0, 5, func(st *orm.QuerySt) string {
		st.Where("id", 3, orm.OP_GE)
		return st.GetWheres()
	}, "id,msg,ip,stime")
	fmt.Println(list)
8、根据条件返回表中指定的一列数据
	ids := sorm.GetColumn(0, 3, func(st *orm.QuerySt) string {
		return st.GetWheres()
	}, "id")
	fmt.Println(ids)
9、根据条件返回指定表的key=>val结构的map,例如返回字典id映射=>名称的map结构
	smap := sorm.GetAsMap(0, -1, func(st *orm.QuerySt) string {
		return st.GetWheres()
	}, "id as `key`, msg as `val`")
	fmt.Println(smap)
10、根据条件返回指定表的map结构,但是这里是key=>item(记录)SqlMap结构,指定一个key隐射到一条记录
	nmap := sorm.GetNameMap(0, 2, func(st *orm.QuerySt) string {
		return st.GetWheres()
	}, "id,ip,msg,stime", "id")
	fmt.Println(nmap)
11、获取查询聚合,例如count(1) 返回查询条件的记录数,SUM(xx)统计累计和的数据等
	total := sorm.GetTotal(func(st *orm.QuerySt) string {
		return st.GetWheres()
	}, "COUNT(1)").ToInt64()
	fmt.Println(total, "===========")
12、查询指定条件的记录是否存在,返回记录ID,例如判定名称是否被使用等等情况
	oldid := sorm.IsExists(func(st *orm.QuerySt) string {
		st.Where("id", 3333)
		return st.GetWheres()
	}).ToInt64()
	fmt.Println(oldid, "====")
13、根据SQL查询表记录,返回SqlMap切片结构
	sql := "SELECT * FROM "+sorm.GetTable()
	slist := sorm.GetAsSQL(sql, 0, 3)
	fmt.Println(slist, "=========end", sql)
14、根据条件设置执行update语句
	sorm.MultiUpdate(func(st *orm.QuerySt) string {
		st.Where("id", 3)
		return st.GetWheres()
	}, func(st *orm.QuerySt) *orm.QuerySt {
		st.Value("stime", time.Now().Unix())
		return st
	})
15、根据条件设置执行delete语句
	sorm.MultiDelete(func(st *orm.QuerySt) string {
		st.Where("id", 111, orm.OP_GE)
		return st.GetWheres()
	})
	return

16、分表策略的管理,这里支持三种分表策略取模/整除/日期归档(适合日志类)
sorm := models.NewOsUser()
sorm->SetModTable(id) //根据id取模做分表,slot=16代表总共16张分表0-15  id%16=?代表在第几张分表
这里会自动检查表是否存在,不存在的话创建表,然后做两层缓存结构,内存缓存、文件缓存,如果内存中记录表已经存在则跳过建表的操作
sorm->SetDevTable(id) //根据id除法分表,例如slot=100w 代表1-100在分表 0 101-200分表1 以此类推
sorm->SetYmTable(id) //根据年或者月份做分表归档处理逻辑

17、sqlmap分表主要利用一个开源的包做结构体到map 或者 map到结构体的逆向反转

18、设置私有库
go env -w GOPRIVATE=gitee.com

19、开始go plugins的支持
使用go plugins的话需要编译开启cgo的支持
CGO_ENABLED=1 go build -buildmode=plugin -o greeter.so main.go
CGO_ENABLED=1 go build -o main demo.go

Documentation

Index

Constants

View Source
const (
	DT_SQL        = "sql"
	DT_AUTO       = "auto"
	OP_AS         = "AS"
	OP_MAX        = "MAX"
	OP_MIN        = "MIN"
	OP_SUM        = "SUM"
	OP_AVG        = "AVG"
	OP_COUNT      = "COUNT"
	OP_EQ         = "="
	OP_NE         = "<>"
	OP_GT         = ">"
	OP_LT         = "<"
	OP_GE         = ">="
	OP_LE         = "<="
	OP_BETWEEN    = "BETWEEN"
	OP_NOTBETWEEN = "NOT BETWEEN"
	OP_LIKE       = "LIKE"
	OP_NOTLIKE    = "NOT LIKE"
	OP_REGEXP     = "REGEXP"
	OP_ISNULL     = "IS NULL"
	OP_ISNOTNULL  = "IS NOT NULL"
	OP_IN         = "IN"
	OP_NOTIN      = "NOT IN"
	OP_AND        = "AND"
	OP_OR         = "OR"
	OP_NOT        = "NOT"
	OP_SQL        = "SQL"
	ASC           = "ASC"
	DESC          = "DESC"
)
View Source
const (
	DBVERCKEY = "dbver"
)

Variables

View Source
var (
	IsShowSql = true
	GdbPoolSt = make(XDBPoolSt)
	GdbConfig = make(map[string]*DbConfig)
)
View Source
var GdbCache cache.Cacher = nil
View Source
var GmCache cache.Cacher = nil
View Source
var Gmodelstpl = `` /* 517-byte string literal not displayed */

Functions

func CamelCase

func CamelCase(str string) string

下划线转成驼峰的格式

func CreateOrmModels

func CreateOrmModels(dbmaster, dbslaver, gdir string)

自动创建模型业务 DB隐射到Model

func CutPath added in v1.0.5

func CutPath(href string) string

将地址切割到path部分

func DT2UnixTimeStamp added in v1.0.5

func DT2UnixTimeStamp(sdt, format string) int64

根据时间格式获取unix时间的记录

func FileExists

func FileExists(path string) bool

判断文件是否存在

func GetMCache

func GetMCache() cache.Cacher

获取内存存储的缓存策略

func IdStr2Slice added in v1.0.5

func IdStr2Slice(idstr, seg, omit string) []string

根据逗号分割的id转slice

func LoadDbConfig

func LoadDbConfig(confPtr interface{})

*

  • 数据库的配置 通过配置导入,配置必须传结构体指针 示例
  • @confPtr *Config 配置对象的指针变量
type Config struct {
	...
	Redis  cache.RedisConfig 	`yaml:"redis"`
	DbMaster  DbConfig 			`yaml:"dbmaster"`
	DbSlaver  DbConfig 			`yaml:"dbslaver"`
}

func NewEngine

func NewEngine(skey string) *sqlx.DB

创建DB对象 提供给其他地方使用

func NewMysql

func NewMysql() *mysqlSt

初始化一个语句结构体对象

func RemoteIp added in v1.0.5

func RemoteIp(req *http.Request) string

获取客户端IP

func SetDBCache

func SetDBCache(c cache.Cacher)

设置数据库缓存 -默认空不做缓存

func SliceSqlMap2Struct

func SliceSqlMap2Struct(ptrSt interface{}, data []SqlMap) error

提供SqlMap转义成struct结构类型 只支持常用类型的

func SnakeCase

func SnakeCase(str string) string

将驼峰的命名格式反转过来

func SqlMap2Struct

func SqlMap2Struct(ptrSt interface{}, data SqlMap) error

提供SqlMap转义成struct结构类型 只支持常用类型的

func SqlMapDeleteItems

func SqlMapDeleteItems(a SqlMap, keys ...string)

删除执行的key信息

Types

type DHandler

type DHandler func(st *QuerySt) interface{}

type DbConfig

type DbConfig struct {
	Driver       string `yaml:"driver"`
	Host         string `yaml:"host"`
	MaxOpenConns int    `yaml:"maxOpenConns"`
	MaxIdleConns int    `yaml:"maxIdleConns"`
}

type ModelSt

type ModelSt struct {
	// contains filtered or unexported fields
}

定义数据模型的基础结构

func (*ModelSt) CacheKey

func (self *ModelSt) CacheKey(ckey string) string

缓存策略的key信息

func (*ModelSt) DBTables added in v1.0.5

func (self *ModelSt) DBTables() []string

获取当前表的所有分表记录

func (*ModelSt) Delete

func (self *ModelSt) Delete(id interface{}) int64

删除一条记录信息

func (*ModelSt) EqMod

func (self *ModelSt) EqMod(idx, oidx int64) bool

func (*ModelSt) GetAsMap

func (self *ModelSt) GetAsMap(offset, limit int64, cHandler WHandler, fields string) SqlMap

只获取数据信息列表key,val

func (*ModelSt) GetAsSQL

func (self *ModelSt) GetAsSQL(sql string, offset, limit int64) []SqlMap

通过SQL查询数据

func (*ModelSt) GetCacheVer

func (self *ModelSt) GetCacheVer() string

获取db存储的版本号

func (*ModelSt) GetColumn

func (self *ModelSt) GetColumn(offset, limit int64, cHandler WHandler, fields string, args ...string) []string

只获取数据信息列表

func (*ModelSt) GetHash

func (self *ModelSt) GetHash(args ...interface{}) string

查询条件hash成md5字符串

func (*ModelSt) GetItem

func (self *ModelSt) GetItem(cHandler WHandler, fields string, args ...string) SqlMap

获取一个选项记录信息

func (*ModelSt) GetList

func (self *ModelSt) GetList(offset, limit int64, cHandler WHandler, fields string, args ...string) []SqlMap

只获取数据信息列表

func (*ModelSt) GetNameMap

func (self *ModelSt) GetNameMap(offset, limit int64, cHandler WHandler, fields string, key string) map[string]SqlMap

获取命名map,key必须属于fields当中的字段

func (*ModelSt) GetOne

func (self *ModelSt) GetOne(id interface{}) SqlMap

获取一个对象实例

func (*ModelSt) GetSlot

func (self *ModelSt) GetSlot() int

func (*ModelSt) GetTable

func (self *ModelSt) GetTable() string

func (*ModelSt) GetTotal

func (self *ModelSt) GetTotal(cHandler WHandler, fields string) SqlString

获取一个选项记录信息

func (*ModelSt) IdClearCache

func (self *ModelSt) IdClearCache(id interface{}) *ModelSt

根据ID删除缓存策略

func (*ModelSt) Init

func (self *ModelSt) Init(dbPool *XDBPoolSt, data map[string]interface{}, fields map[string]reflect.Kind)

初始化模型 业务参数设定

func (*ModelSt) IsExists added in v1.0.5

func (self *ModelSt) IsExists(cHandler WHandler) SqlString

获取一个选项记录信息

func (*ModelSt) MultiDelete

func (self *ModelSt) MultiDelete(whandler WHandler) int64

删除多条记录 单数缓存数据可能还是存在 通过id获取数据的情况

func (*ModelSt) MultiUpdate

func (self *ModelSt) MultiUpdate(whandler WHandler, vhandler VHandler) int64

通过执行匿名函数实现数据更新关系绑定

func (*ModelSt) NewOne

func (self *ModelSt) NewOne(fields SqlMap, dupfields SqlMap) int64

表中新增一条记录 主键冲突且设置dup的话会执行后续配置的更新操作

func (*ModelSt) NewOneFromHandler

func (self *ModelSt) NewOneFromHandler(vhandler VHandler, dhandler DHandler) int64

通过执行匿名函数实现数据更新关系绑定

func (*ModelSt) Query

func (self *ModelSt) Query() *QuerySt

获取db查询的Query db要自行关闭哟

func (*ModelSt) ResetTable

func (self *ModelSt) ResetTable() *ModelSt

func (*ModelSt) Save

func (self *ModelSt) Save(id interface{}, fields SqlMap) int64

更新信息记录

func (*ModelSt) SaveFromHandler

func (self *ModelSt) SaveFromHandler(id interface{}, vhandler VHandler) int64

通过执行匿名函数实现数据更新关系绑定

func (*ModelSt) SetCache added in v1.0.5

func (self *ModelSt) SetCache(cacheSt cache.Cacher) *ModelSt

func (*ModelSt) SetCacheVer

func (self *ModelSt) SetCacheVer()

设定缓存的版本号数据信息

func (*ModelSt) SetDevTable

func (self *ModelSt) SetDevTable(idx int64) *ModelSt

func (*ModelSt) SetModTable

func (self *ModelSt) SetModTable(idx int64) *ModelSt

func (*ModelSt) SetTable added in v1.0.5

func (self *ModelSt) SetTable(table string) *ModelSt

func (*ModelSt) SetYmTable

func (self *ModelSt) SetYmTable(format string) *ModelSt

这个时间格式填写golang诞辰 2006-01-02 15:04:05 等

type QuerySt

type QuerySt struct {
	// contains filtered or unexported fields
}

func NewQuery

func NewQuery(fields map[string]reflect.Kind) *QuerySt

生成一个查询Session

func (QuerySt) AsSql

func (q QuerySt) AsSql(mode string) string

获取语句最终拼凑的SQL语句

func (QuerySt) Clear

func (q QuerySt) Clear(parts ...string) *mysqlSt

清除语句的部分信息

func (*QuerySt) CloseDB added in v1.0.3

func (q *QuerySt) CloseDB()

获取当前执行的查询DB信息

func (*QuerySt) Delete

func (q *QuerySt) Delete() int64

执行数据删除操作

func (QuerySt) Duplicate

func (q QuerySt) Duplicate(field string, value interface{}, args ...string) *mysqlSt

更新字段值设置

func (*QuerySt) Exec added in v1.0.5

func (q *QuerySt) Exec(query string) sql.Result

执行一条SQL语句

func (QuerySt) Field

func (q QuerySt) Field(fields string) *mysqlSt

设置要查询的字段信息

func (*QuerySt) GetAsSql

func (q *QuerySt) GetAsSql(query string, isFirst bool, offset, limit int64) []SqlMap

通过sql语句查询

func (*QuerySt) GetColumn

func (q *QuerySt) GetColumn(query string, offset, limit int64) []string

获取单列信息

func (*QuerySt) GetDb

func (q *QuerySt) GetDb() *sqlx.DB

获取当前执行的查询DB信息 需要及时释放,否则有问题

func (*QuerySt) GetList

func (q *QuerySt) GetList(query string, offset, limit int64) []SqlMap

获取数据信息列表

func (*QuerySt) GetMap

func (q *QuerySt) GetMap(query string, offset, limit int64) SqlMap

获取单列信息 请求设置field 必须 `key`,val结构

func (*QuerySt) GetRow

func (q *QuerySt) GetRow() SqlMap

如果查不到记录返回nil

func (*QuerySt) GetValue

func (q *QuerySt) GetValue() SqlString

获取某个值信息

func (QuerySt) GetWheres

func (q QuerySt) GetWheres() string

获取查询的where数据信息

func (QuerySt) GroupBy

func (q QuerySt) GroupBy(fields ...string) *mysqlSt

设置GroupBy分组

func (QuerySt) Having

func (q QuerySt) Having(having string) *mysqlSt

设置GroupBy Having;一条语句只能设置一次

func (*QuerySt) Insert

func (q *QuerySt) Insert(fields SqlMap, isReplace bool) int64

执行数据插入

func (*QuerySt) NameMap

func (q *QuerySt) NameMap(query, key string, offset, limit int64) map[string]SqlMap

获取数据信息到数组中

func (QuerySt) OrderBy

func (q QuerySt) OrderBy(field, dir string) *mysqlSt

设置排序信息 允许多列排序

func (QuerySt) Reset

func (q QuerySt) Reset() *mysqlSt

结构体初始化内部结构

func (*QuerySt) SetDb added in v1.0.5

func (q *QuerySt) SetDb(db *sqlx.DB) *QuerySt

获取当前执行的查询DB信息 需要及时释放,否则有问题

func (QuerySt) SetIsReplace

func (q QuerySt) SetIsReplace(isReplace bool) *mysqlSt

设定插入的时候执行replace into

func (QuerySt) Table

func (q QuerySt) Table(table ...string) *mysqlSt

设置查询的表信息

func (*QuerySt) Update

func (q *QuerySt) Update(fields SqlMap) int64

执行数据更新操作

func (QuerySt) Value

func (q QuerySt) Value(field string, value interface{}, args ...string) *mysqlSt

更新字段值设置

func (QuerySt) Where

func (q QuerySt) Where(field string, value interface{}, args ...string) *mysqlSt

添加条件配置

type SqlMap

type SqlMap map[string]interface{}

func Struct2SqlMap

func Struct2SqlMap(el interface{}, tagName string) (SqlMap, error)

将结构体转换成map的形式

func (SqlMap) IsNil added in v1.0.5

func (s SqlMap) IsNil() bool

func (SqlMap) ToStruct added in v1.0.5

func (s SqlMap) ToStruct(stPtr interface{}) error

map 直接转成结构体返回

type SqlString added in v1.0.5

type SqlString string

func (SqlString) ToInt64 added in v1.0.5

func (s SqlString) ToInt64() int64

强行转成整数

type VHandler

type VHandler func(st *QuerySt) *QuerySt

type WHandler

type WHandler func(st *QuerySt) string

定义注册到条件的数据资料信息

type XDBPoolSt added in v1.0.5

type XDBPoolSt map[string]*sqlx.DB

*****************************************************************************

	     数据库的适配器,主要调整数据库与配置类 Redis与配置类的衔接,初始化数据库缓存
 *****************************************************************************

func (*XDBPoolSt) Get added in v1.0.5

func (p *XDBPoolSt) Get(skey string) *sqlx.DB

获取数据库连接句柄

func (*XDBPoolSt) Release added in v1.0.5

func (p *XDBPoolSt) Release()

释放db连接句柄信息

Directories

Path Synopsis
Package mapstructure exposes functionality to convert one arbitrary Go type into another, typically to convert a map[string]interface{} into a native Go structure.
Package mapstructure exposes functionality to convert one arbitrary Go type into another, typically to convert a map[string]interface{} into a native Go structure.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL