Django模型层—ORM

一、模型层(models)

Django提供了一个orm(关系映射模型)系统,模型包含了一些字段信息以及查询方法

1-1. 常用的字段类型

AutoField

当model中如果没有自增列,则会自动创建一个列名为id的列,int 自增列,必须填入参数primary_key=True

CharField

字符类型 , 必须提供max_length参数, max_length表示字符长度

EmailField

字符串类型,实际在数据库中只是varchar类型,但Django Admin以及ModelForm中提供验证机制需要用到

SmallIntegerField

小整数 ,32768 ~ 32767

IntegerField

整数列(有符号的) ,2147483648 ~ 2147483647

BigIntegerField

长整型(有符号的),9223372036854775808 ~ 9223372036854775807

BooleanField

布尔值类型

TextField

文本类型

FileField

字符串,路径保存在数据库,文件上传到指定目录 参数:

  1. upload_to = "" 上传文件的保存路径
  2. storage = None 存储组件,默认django.core.files.storage.FileSystemStorage

DecimalField

10进制小数 ,参数:

  1. max_digits,小数总长度
  2. decimal_places,小数位所在占长度

DateTimeField

日期+时间格式, YYYY-MM-DD HH:MM:ss

DateField

日期格式, YYYY-MM-DD

1-2. 字段参数

null

用于表示某个字段可以为空

unique

如果设置为unique=True 则该字段在此表中必须是唯一的

db_index

如果db_index=True 则代表着为此字段设置索引

default

为该字段设置默认值

DateField和DateTimeField

  • auto_now_add

    配置auto_now_add=True,创建数据记录的时候会把当前时间添加到数据库

  • auto_now

    配置上auto_now=True,每次更新数据记录的时候会更新该字段

1-3. 自定义char字段

# 如何自定义字段类型
class MyCharField(models.Field):  # 必须继承Field
    def __init__(self,max_length,*args,**kwargs):
        self.max_length = max_length # 必须以关键字参数形式传参
        # 重新调用父类的方法
        super().__init__(max_length=max_length,*args,**kwargs)


     def db_type(self, connection):
         return 'char(%s)'%self.max_length

1-4. 外键关系

一对一

外键字段创建在任意一张表都可以,建议外键添加在查询频率较高的一方

# 关键字OneToOneField
author_detail = models.OneToOneField(to='Author_detail')  # 外键本质fk + unique

一对多

外键字段创建在多的那一方

# 关键字ForeignKey
publish = models.ForeignKey(to='Publish')  # to用来指代跟哪张表有关系 默认关联的就是表的主键字段
# 外键字段名在创建时会自动加上_id后缀

多对多

外键关系需要创建第三张表来处理。

# 关键字ManyToManyField
author = models.ManyToManyField(to='Author')  
# django orm会自动帮你创建第三张关系表,表名为两个关联的表名用_连接

二、Django中测试脚本的使用

在Django中有一个test.py的测试文件,可用于测试。对于试验orm语句,可以在test中,省略与前端交互的步骤。

在使用前,需要配置好环境,否则测试脚本运行不了

  • 直接在某一个应用下的tests.py文件中书写下面内容(与manage.py前四行代码相似),在内部导入Django模块,再手写两句代码即可。

    ```python
    import os

if name == "main":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "day53.settings")
import django
django.setup()

  # 一定要等待测试脚本搭建完毕之后 才能导入django文件进行测试
  # 必须在内部导入应用文件
from app01 import models
...# 测试代码

```

三、单表操作

3-1. 添加记录

  • 方式一:返回结果是一个对象
# create方法的返回值book_obj就是插入book表中的红楼梦这本书籍纪录对象
book_obj=models.Book.objects.create(name='红楼梦',price=23.8,publish='人民出版社',author='曹雪芹',create_data='2018-09-17')
  • 方式二:先实例化产生对象,然后调用save方法保存
book_obj=models.Book(name='水浒传',price=99,publish='清华出版社',author='施耐庵',create_data='2017-09-17')
book_obj.save()

3-2. 删除记录

  • 方式一:
res=models.Book.objects.filter(name='红楼梦').delete()
# res返回的是被影响行数,queryset对象的.delete()方法
# 如果存在多本红楼梦将会被全部删除
  • 方式二:
res=models.Book.objects.filter(name='红楼梦').first()
res.delete()
# 只删除一个

3-3. 修改记录

  • 方式一:
res=models.Book.objects.filter(name='红楼梦').update(price=20.9)
  • 方式二:
# 对象没有update方法,但是可以用save来修改
book=models.Book.objects.filter(name='红楼梦').first()
book.price=33
book.save()

3-4. 单表查询-必知必会13条

1.all() 
# 查询所有结果,返回的是QuerySet对象
models.Book.objects.all()


2.filter() 
# 指定条件查询,相当于你原生sql语句里面的where关键字,返回的结果QuerySet对象  
# filter内可传多个参数,逗号分隔,他们之间是and关系,不支持负数
# 当条件不存在的情况下,filter不报错直接返回一个空
models.Book.objects.filter(name='红楼梦').first()
models.Book.objects.filter(name='红楼梦')[0]


3.get() 
# 指定条件查询,有且只有一个结果,返回的是对象,是queryset对象,通常使用id查询,不存在或超过一个会报错
# 当条件不存在的情况下,get直接报错 
ret=Book.objects.get(name='红楼梦') 


4.exclude() 
# 返回与筛选对象不匹配的对象,返回的结果QuerySet对象  
models.Book.objects.exclude(price>40)


5.order_by() 
# 对查询结果排序,默认升序,倒序使用-号,可以传多个参数,逗号隔开
models.Book.objects.all().order_by('price') 
models.Book.objects.all().order_by('-price')


6.reverse() 
# 颠倒顺序,必须排好序之后才能颠倒
models.Books.objects.all().order_by('price').reverse()


7.count()
# 查询结果的个数
models.Books.objects.count()

8.first()
# 取queryset中第一个数据对象
models.Books.objects.filter(title='西游记').first()


9.last()
# 取queryset中最后一个数据对象
models.Books.objects.filter(title='西游记').last()


10.exists()
# 如果queryset包含数据就返回True,否则返回False
models.Book.objects.filter(name='红楼梦').exists()


11.values()  
# 获取数据对象中指定的字段的值,可以有多个,返回queryset对象,列表套字典的形式
res = models.Books.objects.values('title','price')


12.values_list() 
# 获取数据对象中指定的字段的值,可以有多个,返回queryset对象,列表套字典的形式
models.Books.objects.values_list('title','price')


13.distinct()从结果中剔除重复记录
# 对查询结果进行去重操作,去重的前提:数据必须是完全相同的情况下,才能够去重(容易忽略主键) 
res = models.Books.objects.values('title','price').distinct()

3-5. orm注意事项

  1. 只要是queryset对象就可以无限制的调用queryset的方法,类似链式操作

  2. orm语句的查询默认都是惰性查询,只有在需要使用时才会执行orm语句

  3. 只要是queryset对象就可以通过.query查看当前结果内部对应的sql语句

  4. 在Django的settings文件中,配置以下信息,就可以查看所有orm对应的内部sql语句

    LOGGING = {    
        'version': 1,    
        'disable_existing_loggers': False,    
        'handlers': {        
            'console':{            
                'level':'DEBUG',            
                'class':'logging.StreamHandler',        
            },    
        },    
        'loggers': {        
            'django.db.backends': {            
                'handlers': ['console'],            
                'propagate': True,            
                'level':'DEBUG',        
            },    
        }}
    

3-6. 单表查询-双下划线

# 基于双下划线的模糊查询
1.大于__gt
models.Book.objects.filter(price__gt='89')

2.小于__lt
models.Book.objects.filter(price__lt='99')

3.小于等于__lte
models.Book.objects.filter(price__lte='99')

4.大于等于__gte
models.Book.objects.filter(price__gte='89')

5.__in  在XX中
ret=models.Book.objects.filter(price__in=['23.8','89','100'])

6.__range 在xx范围内,顾头顾尾
ret=models.Book.objects.filter(price__range=[50,100])

7.__contains 查询名字带有‘红’字的书,默认不忽略大小写
models.Book.objects.filter(name__contains='红')

8.__icontains 查询名字带p的书,忽略大小写
models.Book.objects.filter(name__icontains='P')

9.__startswith 以XX开头
models.Book.objects.filter(name__startswith='红')

10.__endwith 以xx结尾
models.Book.objects.filter(name__endwith='梦')

11.__year  按年查询
models.Book.objects.filter(create_data_year='2018')

12.__month  按月查询
models.Books.objects.filter(publish_date__month='1')

四、多表操作

数据准备

class Book(models.Model):
    title = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=8,decimal_places=2)
    publish_date = models.DateField(auto_now_add=True)
    publish = models.ForeignKey(to='Publish')
    authors = models.ManyToManyField(to='Author')

class Publish(models.Model):
    name = models.CharField(max_length=32)
    addr = models.CharField(max_length=64)
    def __str__(self):
        return self.name


class Author(models.Model):
    name = models.CharField(max_length=32)
    email = models.EmailField() 
    author_detail = models.OneToOneField(to='AuthorDetail')
    def __str__(self):
        return self.name


class AuthorDetail(models.Model):
    phone = models.BigIntegerField()
    addr = models.CharField(max_length=64)
    def __str__(self):
        return self.addr
-------------------------------------------------------    
# 一对一的关系:OneToOneField      模型表的字段,后面会自定加_id
# 一对多的关系:ForeignKey         模型表的字段,后面会自定加_id
# 多对多的关系:ManyToManyField    ManyToManyField会自动创建第三张表

4-1. 一对多

1.添加数据

方式一:

models.Book.objects.create(title='三国演义',price=34.5,publish_id=1) 
# 直接传表里面的实际字段,跟数据主键值publish_id

方式二:

publish_obj = models.Publish.objects.filter(pk=2).first()
# publish_obj=出版社的对象,存到数据库,是一个id
models.Book.objects.create(title='红楼梦',price=444.33,publish=publish_obj)  
# 传虚拟字段跟数据对象即可

2.修改数据

方式一:

models.Book.objects.filter(pk=1).update(publish_id=2)
# 括号内放的是publish_id这个数据库实际字段

方式二:

publish_obj = models.Publish.objects.filter(pk=1).first()
models.Book.objects.filter(pk=1).update(publish=publish_obj)
# 括号内放的是publish,在models创建的字段名

3.删除数据

方式一:

Copyret=models.Book.objects.filter(name='红楼梦').delete()
# ret返回的是被影响行数,queryset对象的.delete()方法
如果存在多本红楼梦将会被全部删除

方式二:

models.Publish.objects.filter(pk=1).delete()  
# 默认就是级联删除 级联更新

4-2. 多对多

1.添加数据 add()

# 给当前这一本书绑定作者
# 方式一
book_obj = models.Book.objects.filter(pk=2).first()
# book_obj.authors就是跳到第三张表
book_obj.authors.add(1,2)  # 在第三张表里面给书籍绑定主键为1,2的作者

# 方式二
author_obj = models.Author.objects.filter(pk=1).first()
author_obj1 = models.Author.objects.filter(pk=2).first()
# add中添加对象
book_obj.authors.add(author_obj,author_obj1)


"""
add方法 能够朝第三张关系表添加数据
即支持传数字,add(1,2) , 也支持传对象add(author_obj,author_obj1) , 并且两者都可以是多个
"""

2.删除数据 remove()

# 删除主键为2的书籍的作者
book_obj = models.Book.objects.filter(pk=2).first()
book_obj.authors.remove(1,2)


author_obj = models.Author.objects.filter(pk=1).first()
author_obj1 = models.Author.objects.filter(pk=2).first()
book_obj.authors.remove(author_obj,author_obj1)
"""
remove既可以传主键数字,也可以传对象,并且都支持传多个, remove(1,2) , remove(author_obj,author_obj1)
"""

3.清空数据 clear()

# 清空,括号内不需要传参 , 删除某个数据在第三张表中的所有记录
# 删除主键为2的书籍的所有相关记录
book_obj = models.Book.objects.filter(pk=2).first()
book_obj.authors.clear()

4.修改数据 set()

# 修改主键为2的书籍的作者
book_obj = models.Book.objects.filter(pk=2).first()
book_obj.authors.set((1,3))
# book_obj.authors.set([1,])   # set括号内必须是可迭代对象,如元组、列表

author_obj = models.Author.objects.filter(pk=1).first()
author_obj1 = models.Author.objects.filter(pk=2).first()
book_obj.authors.set((author_obj,author_obj1))

"""
set修改多对多关系表中的数据,既可以传数字也可以传对象, 但是需要注意的是括号内必须是可迭代对象
都支持多个 , set((1,3)) , set((author_obj,author_obj1))
"""

4-3. 基于对象的跨表查询

# 基于对象的查询是子查询也就是多次查询
跨表查询,需要分清楚正向和反向的区别

1. 正向查询:关系字段创建在表1,由表1查询表2,就是正向。正向查询按字段
2. 反向查询:关系字段创建在表1,由表2查询表1,就是反向。反向查询按表名小写 + _set

1.一对一 查询数据

正向查询  按字段
反向查询  按表名小写,不需要加_set

正向查询:查询作者是xxx的手机号码

author_obj = models.Author.objects.filter(name='xxx').first()
print(author_obj.author_detail)  # author_detail是作者表中的外键字段名
# author_obj.author_detail 就是作者详情的对象
print(author_obj.author_detail.phone)

反向查询:查询手机号是123的作者姓名

author_detail_obj = models.AuthorDetail.objects.filter(phone=123).first()
print(author_detail_obj.author)  # author是小写的作者表名,得到个作者对象
print(author_detail_obj.author.name)

2.一对多

正向查询  按字段
反向查询  按表名小写+_set , 如果有多条数据再加.all()

正向查询:查询红楼梦这本书的出版社

book_obj=Book.objects.filter(name='红楼梦').first()
print(book_obj.publish)   # 正向查询按字段,直接.字段名 ,获得出版社对象
print(book_obj.publish.name)  # 获得出版社对象的名字

反向查询:查询出版社是上海出版社出版过的书籍

publish_obj = models.Publish.objects.filter(name='东方出版社').first()
print(publish_obj.book_set)  # 多对多的反向查询需要写 book_set (小写表名_set)  
# 因为有多条数据,需要加.all()
print(publish_obj.book_set.all())

3.多对多

正向查询  按字段
反向查询  按表名小写+_set , 如果有多条数据再加.all()

正向查询:查询红楼梦这本书的所有作者

book_obj = models.Book.objects.filter(title='红楼梦').first()
print(book_obj.authors)  # 正向查询用字段,authors是书籍表的外键字段名
# 如果有多条数据就用all()
print(book_obj.authors.all())

反向查询:查询作者是xxx写过的书籍

author_obj = models.Author.objects.filter(name='jason').first()
print(author_obj.book_set)  # 多对多的反向查询需要写 book_set (小写表名_set)
# 有多条数据就用all()
print(author_obj.book_set.all())

4-4. 基于双下划线查询

1.一对一

正向:按字段,跨表可以在filter,也可以在values中    按字段
反向:按表名小写,跨表可以在filter,也可以在values中    按表名小写
# models后面点的哪张表 , 就以哪张表为基表

正向查询:查询作者xxx的手机号

res=models.Author.object.filter(name='xxx').values('author_detail__phone')
# 基表为作者表,所以values可以通过作者表的author_detail字段 跨到详情表,从而获取手机号

反向查询:查询作者xxx的手机号

res=models.AuthorDetail.object.filter(author__name='xxx').values('phone')
# 基表为详情表,在filter内跨表

2.一对多

正向查询:查询出版社为上海出版社出版的所有图书的名字,价格

res=models.Book.objects.filter(publish_name='上海出版社').values('name','price')

反向查询

res=models.Publish.objects.filter(name='上海出版社').values('book_name','book_price')

3.多对多

正向查询:查询红楼梦的所有作者名字

res=models.Book.objects.filter(title='红楼梦').values('authors_name')

反向查询:查询红楼梦的所有作者名字

res=models.Author.objects.filter(book__title='红楼梦').values('name')

4.联表操作

# 1.查询书籍pk为2的出版社名称
正向查询:基于有外键字段的书籍表查询
res = models.Book.objects.filter(pk=2).values('publish__name')  
# 写外键字段就相当于已经跨到外键字段所关联的表
print(res)

反向查询:基于没有外键关系的出版社表查询
res = models.Publish.objects.filter(book__pk=2).values('name')
# 你想要改表的哪个字段信息 你只需要加__获取即可
print(res)



# 2.查询书籍pk为2的作者姓名和邮箱
正向查询:基于有外键字段的书籍表查询
res = models.Book.objects.filter(pk=2).values('authors__name','authors__email')
print(res)

反向查询:基于没有外键关系的出版社表查询
res = models.Author.objects.filter(book__pk=2).values('name','email')
print(res)

五、聚合查询

聚合查询需要用到一个关键字aggregate(),并配合聚合函数一起使用。

用到的内置函数:

from django.db.models import Avg, Sum, Max, Min, Count

例子:

from django.db.models import Avg, Sum, Max, Min, Count
# 筛选出价格最高的书籍的,默认聚合值名为 price__max (字段名__聚合函数名)
res = models.Book.objects.aggregate(Max('price'))

也可以为聚合值指定一个名称

# 求书籍总价格,为得到的值取price_sum
res = models.Book.objects.aggregate(price_sum = Sum('price'))

生成多个聚合aggregate()可添加多个参数

# 查询书籍最高价格、最低价格、价格总和、记录条数、平均价格
res =models.Book.objects.aggregate(
    Max('price'),Min('price'),Sum('price'),Count('price'),Avg('price')
)

六、分组查询

分组查询需要用到annotate()关键字,分组查询一般是和聚合函数连用

 1.统计每一本书的作者个数 书名 和对应的作者人数
res = models.Book.objects.annotate(
    author_num=Count('authors__id')).values('title','author_num')

2.统计出每个出版社卖的最便宜的书的价格  出版社的名字 价格
res = models.Publish.objects.annotate(
    min_price=Min('book__price')).values('name','min_price')

3.统计不止一个作者的图书
res = models.Book.objects.annotate(
   author_num=Count('authors')).filter(author_num__gt=1).values('title','author_num')
 
4.查询各个作者出的书的总价格,作者名字,总价格
res = models.Author.objects.annotate(
    sum_price=Sum('book__price')).values('name','sum_price')

总结:

1.values在annotate前表示分组,在后表示取值

2.filter在annotate前表示where条件,在后表示having

3.annotate本身表示group by的作用,前面找寻分组依据,内部放置显示可能用到的聚合运算式,后面跟filter来增加限制条件,最后的value来表示分组后想要查找的字段值

七、F查询

F查询:对两个字段的值做比较
F能够获取表中字段所对应的值,F()的实例可以在查询中引用字段,来比较同一个model实例中两个不同字段的值

例子:

# 查询库存大于卖出的书籍
res = models.Book.objects.filter(kun_cun__gt = F('mai_cun')).values('title')

Django支持F()对象之间以及F()对象和常数之间的算术运算和取模的操作

例子:

# 将每本书的价格提升100
res = models.Book.objects.all().update(F('price')+100)

如果要修改char字段的数据,要对字符串进行拼接Concat操作,并且要加上拼接值value

例子:

# 在所有书的名字后面加上“爆款”
from django.db.models.functions import Concat
from django.db.models import Value
res = models.Book.objects.update(title=Concat(F('title'),Value('爆款')))

八、Q查询

filter()等方法进行的是'AND',更复杂的查询需要Q查询
表示与 & ,或 | ,非 ~
#查询是作者A也或者是作者B的书
Book.object.all().filter(Q(author_name='A')|Q(author_name='B'))

filter()等方法中逗号隔开的条件是 与(and)的关系。

#查询书名是三国演义 and 库存为500的书
res = models.Book.objects.filter(Q(title='三国演义'),Q(kun_cun=500))

#查询书名是三国演义 or 库存为500的书
res = models.Book.objects.filter(Q(title='三国演义')|Q(kun_cun=500))

#查询书名是三国演义 not 库存为500的书
res = models.Book.objects.filter(~Q(title='三国演义')|Q(kun_cun=500))

Q对象高级用法

# 不适用字段名,使用字符串
q = Q() # 实例化一个对象q
q.connector = 'or'  # 默认是and 可以改为or
q.children.append(('title','三国演义'))
q.children.append(('kun_cun__gt', 500))
res = models.Book.objects.filter(q)

九、ORM中的事务操作

9-1. 什么是事务

将多个sql语句操作编程原子性操作,要么同时成功,否则有一个失败则里面回滚到原来的状态,保证数据的完整性和一致性

9-2. 事务的特性(ACID)

  1. 原子性(Atomicity)

    要么都执行,要么都不执行。

    假如我们有个方法中对一个属性进行了N次的更新,但是执行到一半的时候有一个语句有问题出现了异常,这样就可能使得我们上面所说的操作后的点与我们预先的点不同,这不是我们想要的,所以原子性要求你这个方法要么全部执行成功,要么你就别执行。

  2. 一致性(Consistency)

    只有两种状态:提交成功前、提交成功后。不会有中间状态,如修改了一半的。

    原子性中规定方法中的操作都执行或者都不执行,但并没有说要所有操作一起执行(一起更新那就乱套了,要哪个结果?),所以操作的执行也是有先后顺序的,那我们要是在执行一半时查询了数据库,那我们会得到中间的更新的属性?答案是不会的,一致性规定事务提交前后只存在两个状态,提交前的状态和提交后的状态,绝对不会出现中间的状态。

  3. 隔离性(Isolation)

    串行化,使得事务执行不会混乱。

    事务的隔离性基于原子性和一致性,每一个事务可以并发执行,但是他们互不干扰,但是也有可能不同的事务会操作同一个资源,这个时候为了保持隔离性会用到锁方案。

  4. 持久性(Durability)

    事务成功执行后,修改的状态将一直保持。

    当一个事务提交了之后那这个数据库状态就发生了改变,哪怕是提交后刚写入一半数据到数据库中,数据库宕机(死机)了,那当你下次重启的时候数据库也会根据提交日志进行回滚,最终将全部的数据写入。

十, 数据库三大范式

数据库设计范式

什么是范式:简言之就是,数据库设计对数据的存储性能,还有开发人员对数据的操作都有莫大的关系。所以建立科学的,规范的的数据库是需要满足一些

规范的来优化数据数据存储方式。在关系型数据库中这些规范就可以称为范式。

什么是三大范式:

第一范式:当关系模式R的所有属性都不能在分解为更基本的数据单位时,称R是满足第一范式的,简记为1NF。满足第一范式是关系模式规范化的最低要

求,否则,将有很多基本操作在这样的关系模式中实现不了。

第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式,简记为2NF。

第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖于R的任意一个候选关键字,称R满足第三范式,简记为3NF.

注:关系实质上是一张二维表,其中每一行是一个元组,每一列是一个属性

理解三大范式

第一范式

1、每一列属性都是不可再分的属性值,确保每一列的原子性

2、两列的属性相近或相似或一样,尽量合并属性一样的列,确保不产生冗余数据。

img

img

如果需求知道那个省那个市并按其分类,那么显然第一个表格是不容易满足需求的,也不符合第一范式。

img

img

显然第一个表结构不但不能满足足够多物品的要求,还会在物品少时产生冗余。也是不符合第一范式的。

第二范式

每一行的数据只能与其中一列相关,即一行数据只做一件事。只要数据列中出现数据重复,就要把表拆分开来。

img

一个人同时订几个房间,就会出来一个订单号多条数据,这样子联系人都是重复的,就会造成数据冗余。我们应该把他拆开来。

img

img

这样便实现啦一条数据做一件事,不掺杂复杂的关系逻辑。同时对表数据的更新维护也更易操作。

第三范式

数据不能存在传递关系,即没个属性都跟主键有直接关系而不是间接关系。像:a-->b-->c 属性之间含有这样的关系,是不符合第三范式的。

比如Student表(学号,姓名,年龄,性别,所在院校,院校地址,院校电话)

这样一个表结构,就存在上述关系。 学号--> 所在院校 --> (院校地址,院校电话)

这样的表结构,我们应该拆开来,如下。

(学号,姓名,年龄,性别,所在院校)--(所在院校,院校地址,院校电话)

最后:

三大范式只是一般设计数据库的基本理念,可以建立冗余较小、结构合理的数据库。如果有特殊情况,当然要特殊对待,数据库设计最重要的是看需求跟性能,需求>性能>表结构。所以不能一味的去追求范式建立数据库。

posted @ 2019-11-29 21:54  ^啷个哩个啷$  阅读(125)  评论(0编辑  收藏  举报