SELECT JOIN

SELECT JOIN

SQL 语法分析

JOIN 查询有点像我们的 Expression,就是可以 查询套查询无限套下去。

MySQL

image.png
image.png

SQLite

image.png
image.png
image.png

PostgreSQL

和 MySQL、SQLite 也差不多
image.png

JOIN 语法总结

** JOIN 语法有两种形态 **
image.png

  • JOIN … ON
  • JOIN … USING:USING 后面使用的是列

** JOIN 本身有:**

  • INNER JOIN, JOIN
  • LEFT JOIN,RIGHT JOIN

也就是说 JOIN 的结构大概可以描述成下面这几种,而且还可以嵌套。

  • 表 JOIN 表
  • (表 JOIN 表) JOIN 表
  • 表 JOIN 子查询
  • 子查询 JOIN 子查询

开源实例

Beego JOIN 查询

** Beego 的 JOIN 查询主要出现在两个地方 :**
QueryBuilder 接口设计了 InnerJoin、LeftJoin 和 RightJoin 三个方法
image.png
Beego 本身支持一对一、一对多和多对多的关联关 系,所以如果设置了正确的关联关系,那么 Beego 在部分情况下会生成 JOIN 查

GROM JOIN 查询

GORM中 JOIN 查询主要是为了所谓的 Preload 而服务的
image.png

相关接口与方法的设计

实现简单的 JOIN 其实不怎么复杂,主要功能包括起别名、选择列、复杂一点的是 JOIN 可以嵌套。
常用的 JOIN 结构大概就是下面这几个:

  • 表 A JOIN 表 B ON …
  • 表 A AS 新名字 JOIN 表 B AS 新名字 ON …
  • 表 A JOIN (表 B JOIN 表 C ON …) ON …

之前处理 FROM 后面那个位置的时候是直接用的数据库表名,但是那个位置其实可以放的玩意有表名、JOIN、子查询,这明摆了是要有一个抽象的,官方文档也告诉你了,叫 table_references(这里叫表表达式)。但是这三种对象的处理方式肯定是不一样的,语句形态差的都很远,基本没有共同点。

TableReference 抽象

  • Table: 代表普通的表
  • Join:代表 JOIN 查询
  • Subquery:子查询

image.png

type TableReference interface {
	tableAlias() string
}

TableReference 可以在将来有需要的时候 不断增加方法。

JoinBuilder 和 Join 定义

image.png

var _ TableReference = Join{}

type JoinBuilder struct {
    left TableReference
    right TableReference
    typ string
}

type Join struct {
    left TableReference
    right TableReference
    typ string
    on []Predicate
    using []string
}

func (j Join) tableAlias() string {
	return ""
}

JoinBuilder 里面的 On 和 Using 是终结方法,也就是 直接返回了 Join , 这种设计可以避免用户同时调用 On 或者 Using。

func (j *JoinBuilder) On(ps...Predicate) Join {
	return Join {
		left: j.left,
		right: j.right,
		on: ps,
		typ: j.typ,
	}
}

func (j *JoinBuilder) Using(cs...string) Join {
	return Join {
		left: j.left,
		right: j.right,
		using: cs,
		typ: j.typ,
	}
}

JOIN 本身也可以进一步 JOIN,所以我们 同样需要在 Join 上面定义类似的方法。 同样地,子查询也可以用来构造 JOIN 查 询。

func (j Join) Join(target TableReference) *JoinBuilder {
	return &JoinBuilder{
		left:  j,
		right: target,
		typ:   "JOIN",
	}
}

func (j Join) LeftJoin(target TableReference) *JoinBuilder {
	return &JoinBuilder{
		left:  j,
		right: target,
		typ:   "LEFT JOIN",
	}
}

func (j Join) RightJoin(target TableReference) *JoinBuilder {
	return &JoinBuilder{
		left:  j,
		right: target,
		typ:   "RIGHT JOIN",
	}
}

Table ( 普通表 )

Table 代表一个普通的表,它也是 JOIN 查询的起点

type Table struct {
	entity any
	alias string
}

func TableOf(entity any) Table {
	return Table{
		entity: entity,
	}
}

func (t Table) tableAlias() string {
	return t.alias
}

func (t Table) As(alias string) Table {
	return Table {
		entity: t.entity,
		alias: alias,
	}
}

func (t Table) Join(target TableReference) *JoinBuilder {
	return &JoinBuilder{
		left: t,
		right: target,
		typ: "JOIN",
	}
}

func (t Table) LeftJoin(target TableReference) *JoinBuilder {
	return &JoinBuilder{
		left: t,
		right: target,
		typ: "LEFT JOIN",
	}
}

func (t Table) RightJoin(target TableReference) *JoinBuilder {
	return &JoinBuilder{
		left: t,
		right: target,
		typ: "RIGHT JOIN",
	}
}

重构 Selector

将 From 改为接收一个 TableReference 作为 输入,后续 subquery 同样可以复用这个方法。

func (s *Selector[T]) From(tbl TableReference) *Selector[T] {
	s.table = tbl
	return s
}
  • case nil : 如果用户没有调用 From 方法
  • case Table:用户传入了一个普通的表
  • case Join:是一个 Join 查询
func (s *Selector[T]) buildTable(table TableReference) error {
	switch tab := table.(type) {
	case nil:
		s.quote(s.model.TableName)
	case Table:
		m, err := s.r.Get(tab.entity)
		if err != nil {
			return err
		}
		s.quote(m.TableName)
		if tab.alias != "" {
			s.sb.WriteString(" AS ")
			s.quote(tab.alias)
		}
	case Join:
		return s.buildJoin(tab)
	default:
		return errs.NewErrUnsupportedExpressionType(tab)
	}
	return nil
}

func (s *Selector[T]) buildJoin(tab Join) error {
	s.sb.WriteByte('(')
	if err := s.buildTable(tab.left); err != nil {
		return err
	}
	s.sb.WriteString(" ")
	s.sb.WriteString(tab.typ)
	s.sb.WriteString(" ")
	if err := s.buildTable(tab.right); err != nil {
		return err
	}
	if len(tab.using) > 0 {
		s.sb.WriteString(" USING (")
		for i, col := range tab.using {
			if i > 0 {
				s.sb.WriteByte(',')
			}
			err := s.buildColumn(Column{name: col}, false)
			if err != nil {
				return err
			}
		}
		s.sb.WriteString(")")
	}
	if len(tab.on) > 0 {
		s.sb.WriteString(" ON ")
		err := s.buildPredicates(tab.on)
		if err != nil {
			return err
		}
	}
	s.sb.WriteByte(')')
	return nil
}

重构列校验逻辑

在原本不支持 JOIN 查询的时候,只需要 看一下操作的元数据里面有没有这个列。在支持了 JOIN 之后,那么所有的列、聚合函 数都可能有一个拥有者(owner)。 例如** t1.col1 其中 t1 就是 col1 的拥有者**。 那么校验逻辑就是:

  • **如果用户指定了表,那么就检查指定的表上 面有没有这个列 **
  • 如果没有指定表,就走老逻辑,也就是右图 case nil 的分支
func (s *Selector[T]) buildColumns() error {
	if len(s.columns) == 0 {
		s.sb.WriteByte('*')
		return nil
	}
	for i, c := range s.columns {
		if i > 0 {
			s.sb.WriteByte(',')
		}
		switch val := c.(type) {
		case Column:
            // buildColumn 方法
			if err := s.buildColumn(val, true); err != nil {
				return err
			}
		case Aggregate:
			if err := s.buildAggregate(val, true); err != nil {
				return err
			}
		case RawExpr:
			s.raw(val)
		default:
			return errs.NewErrUnsupportedSelectable(c)
		}
	}
	return nil
}

buildColumn 方法

func (s *Selector[T]) buildColumn(c Column, useAlias bool) error {
	err := s.builder.buildColumn(c.table, c.name)
	if err != nil {
		return err
	}
	if useAlias {
		s.buildAs(c.alias)
	}
	return nil
}
// buildColumn 构造列
// 如果 table 没有指定,我们就用 model 来判断列是否存在
func (b *builder) buildColumn(table TableReference, fd string) error {
	var alias string
	if table != nil {
		alias = table.tableAlias()
	}
	if alias != "" {
		b.quote(alias)
		b.sb.WriteByte('.')
	}
	colName, err := b.colName(table, fd)
	if err != nil {
		return err
	}
	b.quote(colName)
	return nil
}

func (b *builder) colName(table TableReference, fd string) (string, error) {
	switch tab := table.(type) {
	case nil:
		fdMeta, ok := b.model.FieldMap[fd]
		if !ok {
			return "", errs.NewErrUnknownField(fd)
		}
		return fdMeta.ColName, nil
	case Table:
		m, err := b.r.Get(tab.entity)
		if err != nil {
			return "", err
		}
		fdMeta, ok := m.FieldMap[fd]
		if !ok {
			return "", errs.NewErrUnknownField(fd)
		}
		return fdMeta.ColName, nil
	case Join:
		colName, err := b.colName(tab.left, fd)
		if err != nil {
			return colName, nil
		}
		return b.colName(tab.right, fd)
	default:
		return "", errs.NewErrUnsupportedExpressionType(tab)
	}
}

总结

  • GORM 的 Preload 是什么?本质上就是一个 JOIN 查询,并且严格来说,在 Go 语言里面是很难实现 lazy load的。GORM 的 Preload 就是通过 Join 把相关的数据都查询出来,并且组装成结构体 ;
  • WHERE、ON 和 HAVING 的区别:在 JOIN 查询里面,一般的建议 都是尽量把条件放到 ON 上面,这样 JOIN 生成的中间数据要少很多;
  • JOIN 的执行原理:可以近似理解为一个双重循环 。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值