day 64 模型层-单表 (M)

1 ORM简介#

img

​ 我们在使用Django框架开发web应用的过程中,不可避免地会涉及到数据的管理操作(如增、删、改、查),而一旦谈到数据的管理操作,就需要用到数据库管理软件,例如mysql、oracle、Microsoft SQL Server等。

如果应用程序需要操作数据(比如将用户注册信息永久存放起来),那么我们需要在应用程序中编写原生sql语句,然后使用pymysql模块远程操作mysql数据库。

​ 但是直接编写原生sql语句会存在两方面的问题,严重影响开发效率,如下

1. sql语句的执行效率:应用开发程序员需要耗费一大部分精力去优化sql语句
2. 数据库迁移:针对mysql开发的sql语句无法直接应用到oracle数据库上,一旦需要迁移数据库,便需要考虑跨平台问题

​ 为了解决上述问题,django引入了ORM的概念,ORM全称Object Relational Mapping,即对象关系映射,是在pymysq之上又进行了一层封装,对于数据的操作,我们无需再去编写原生sql,取代代之的是基于面向对象的思想去编写类、对象、调用相应的方法等,ORM会将其转换/映射成原生SQL然后交给pymysql执行。

插图1

插图2

​ 如此,开发人员既不用再去考虑原生SQL的优化问题,也不用考虑数据库迁移的问题,ORM都帮我们做了优化且支持多种数据库,这极大地提升了我们的开发效率,下面就让我们来详细学习ORM的使用吧

2 单表操作#

2.1 按步骤创建表#

2.1.1 建项目#

创建django项目,新建名为app01的app,在app01的models.py中创建模型

class Employee(models.Model): 	# 必须是models.Model的子类
    id=models.AutoField(primary_key=True)

    name=models.CharField(max_length=16)

    gender=models.BooleanField(default=1)

    birth=models.DateField()

    department=models.CharField(max_length=30)

    salary=models.DecimalField(max_digits=10,decimal_places=1)

2.1.2 改数据结构#

django的orm支持多种数据库,如果想将上述模型转为mysql数据库中的表,需要settings.py中

# 删除\注释掉原来的DATABASES配置项,新增下述配置
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', # 使用mysql数据库
        'NAME': 'db1',          # 要连接的数据库
        'USER': 'root',         # 链接数据库的用于名
        'PASSWORD': '',         # 链接数据库的用于名                  
        'HOST': '127.0.0.1',    # mysql服务监听的ip  
        'PORT': 3306,           # mysql服务监听的端口  
        'ATOMIC_REQUEST': True, 
        "设置为True代表同一个http请求所对应的所有sql都放在一个事务中执行 (要么所有都成功,要么所有都失败),这是全局性的配置,如果要对某个http请求放水(然后自定义事务),可以用non_atomic_requests修饰器 "
        'OPTIONS': {
            "init_command": "SET storage_engine=INNODB", #设置创建表的存储引擎为INNODB
        }
    }
}

2.1.3 建库#

在链接mysql数据库前,必须事先创建好数据库

mysql> create database db1; # 数据库名必须与settings.py中指定的名字对应上

2.1.4 修改为pymysql模块#

​ 其实python解释器在运行django程序时,django的orm底层操作数据库的python模块默认是mysqldb而非pymysql,然而对于解释器而言,python2.x解释器支持的操作数据库的模块是mysqldb,而python3.x解释器支持的操作数据库的模块则是pymysql,毫无疑问,目前我们的django程序都是运行于python3.x解释器下,于是我们需要修改django的orm默认操作数据库的模块为pymysql,具体做法如下

插图3

2.1.5 注册app#

​ 确保配置文件settings.py中的INSTALLED_APPS中添加我们创建的app名称,django2.x与django1.x处理添加方式不同

# django1.x版本,在下述列表中新增我们的app名字即可
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01',
    # 'app02' # 若有新增的app,依次添加即可
]


# django2.x版本,可能会帮我们自动添加app,只是换了一种添加方式
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config', 	# 如果默认已经添加了,则无需重复添加
     'app02.apps.App02Config', 	# 若有新增的app,按照规律依次添加即可
]

2.1.6 打印sql语句#

如果想打印orm转换过程中的sql,需要在settings中进行配置日志:

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

2.1.7 迁移数据库#

最后在命令行中执行两条数据库迁移命令,即可在指定的数据库db1中创建表 :

$ python manage.py makemigrations
$ python manage.py migrate

 注意:
	1、makemigrations只是生成一个数据库迁移记录的文件,而migrate才是将更改真正提交到数据库执行
	2、数据库迁移记录的文件存放于app01下的migrations文件夹里
	3、了解:使用命令python manage.py showmigrations可以查看没有执行migrate的文件

注意1:在使用的是django1.x版本时,如果报如下错误

django.core.exceptions.ImproperlyConfigured: mysqlclient 1.3.3 or newer is required; you have 0.7.11.None
那是因为MySQLclient目前只支持到python3.4,如果使用的更高版本的python,需要找到文件
	C:\Programs\Python\Python36-32\Lib\site-packages\Django-2.0-
	py3.6.egg\django\db\backends\mysql\base.py
这个路径里的文件(mac或linux请在终端执行pip show django查看django的安装路径,找到base.py即可)
# 注释下述两行内容即可
if version < (1, 3, 3):
     raise ImproperlyConfigured("mysqlclient 1.3.3 or newer is required; you have %s" % Database.__version__)

注意2:当我们直接去数据库里查看生成的表时,会发现数据库中的表与orm规定的并不一致,这完全是正常的,事实上,orm的字段约束就是不会全部体现在数据库的表中,比如我们为字段gender设置的默认值default=1,去数据库中查看会发现该字段的default部分为null

mysql> desc app01_employee; # 数据库中标签前会带有前缀app01_
+------------+---------------+------+-----+---------+----------------+
| Field      | Type          | Null | Key | Default | Extra          |
+------------+---------------+------+-----+---------+----------------+
| id         | int(11)       | NO   | PRI | NULL    | auto_increment |
| name       | varchar(16)   | NO   |     | NULL    |                |
| gender     | tinyint(1)    | NO   |     | NULL    |                |
| birth      | date          | NO   |     | NULL    |                |
| department | varchar(30)   | NO   |     | NULL    |                |
| salary     | decimal(10,1) | NO   |     | NULL    |                |
+------------+---------------+------+-----+---------+----------------+

虽然数据库没有增加默认值,但是我们在使用orm插入值时,完全为gender字段插入空,orm会按照自己的约束将空转换成默认值后,再提交给数据库执行

2.1.8 修改表#

在表生成之后,如果需要增加、删除、修改表中字段,需要这么做

 一:增加字段
	1.1、在模型类Employee里直接新增字段,强调:对于orm来说,新增的字段必须用default指定默认值
publish = models.CharField(max_length=12,default='人民出版社',null=True)
	1.2、重新执行那两条数据库迁移命令


二:删除字段
	2.1 直接注释掉字段
	2.2 重新执行那两条数据库迁移命令

 三:修改字段
	3.1 将模型类中字段修改
	3.2 重新执行那两条数据库迁移命令

2.2 添加记录#

方式一:

# 1、用模型类创建一个对象,一个对象对应数据库表中的一条记录
obj = Employee(name="Egon", gender=0, birth='1997-01-27', department="财务部", salary=100.1)
# 2、调用对象下的save方法,即可以将一条记录插入数据库
obj.save()

方式二:

# 每个模型表下都有一个objects管理器,用于对该表中的记录进行增删改查操作,其中增加操作如下所示
obj = Employee.objects.create(name="Egon", gender=0, birth='1997-01-27', department="财务部", salary=100.1)

2.3 查询记录#

2.3.1 查询API#

​ 模型Employee对应表app01_employee,表app01_employee中的每条记录都对应类Employee的一个对象,我们以该表为例,来介绍查询API,读者可以自行添加下述记录,然后配置url、编写视图测试下述API

mysql> select * from app01_employee;
+----+-------+--------+------------+------------+--------+
| id | name  | gender | birth      | department | salary |
+----+-------+--------+------------+------------+--------+
|  1 | Egon  |      0 | 1997-01-27 | 财务部     |  100.1 |
|  2 | Kevin |      1 | 1998-02-27 | 技术部     |   10.1 |
|  3 | Lili  |      0 | 1990-02-27 | 运营部     |   20.1 |
|  4 | Tom   |      1 | 1991-02-27 | 运营部     |   30.1 |
|  5 | Jack  |      1 | 1992-02-27 | 技术部     |   11.2 |
|  6 | Robin |      1 | 1988-02-27 | 技术部     |  200.3 |
|  7 | Rose  |      0 | 1989-02-27 | 财务部     |   35.1 |
|  8 | Egon  |      0 | 1997-01-27 | 财务部     |  100.1 |
|  9 | Egon  |      0 | 1997-01-27 | 财务部     |  100.1 |
+----+-------+--------+------------+------------+--------+

​ 每个模型表下都有一个objects管理器,用于对该表中的记录进行增删改查操作,其中查询操作如下所示

Part1:

​ 强调:下述方法(除了count外)的返回值都是一个模型类Employee的对象,为了后续描述方便,我们统一将模型类的对象称为"记录对象",每一个”记录对象“都唯一对应表中的一条记录,

# 1. get(**kwargs)
    1.1: 有参,参数为筛选条件
    1.2: 返回值为一个符合筛选条件的记录对象(有且只有一个),如果符合筛选条件的对象超过一个或者没有都会抛出错误。
    obj=Employee.objects.get(id=1)
    print(obj.name,obj.birth,obj.salary) #输出:Egon 1997-01-27 100.1

# 2、first()
    2.1:无参
    2.2:返回查询出的第一个记录对象
    obj=Employee.objects.first() 	# 在表所有记录中取第一个
    print(obj.id,obj.name) 	# 输出:1 Egon

# 3、last()
    3.1: 无参
    3.2: 返回查询出的最后一个记录对象
    obj = Employee.objects.last() # 在表所有记录中取最后一个
    print(obj.id, obj.name)  # 输出:9 Egon

# 4、count():
    4.1:无参
    4.2:返回包含记录对象的总数量
    res = Employee.objects.count() # 统计表所有记录的个数
    print(res) # 输出:9

# 注意:如果我们直接打印Employee的对象将没有任何有用的提示信息,我们可以在模型类中定义__str__来进行定制
class Employee(models.Model):
	......
	# 在原有的基础上新增代码如下
	def __str__(self):
		return "<%s:%s>" %(self.id,self.name)
    
"此时我们print(obj)显示的结果就是: <本条记录中id字段的值:本条记录中name字段的值>"

Part2:

​ 强调:下述方法查询的结果都有可能包含多个记录对象,为了存放查询出的多个记录对象,django的ORM自定义了一种数据类型Queryeset,所以下述方法的返回值均为QuerySet类型的对象,QuerySet对象中包含了查询出的多个记录对象

# 1、filter(**kwargs):
	1.1:有参,参数为过滤条件
	1.2:返回值为QuerySet对象,QuerySet对象中包含了符合过滤条件的多个记录对象
	queryset_res=Employee.objects.filter(department='技术部')
"	print(queryset_res) # 输出: <QuerySet [<Employee: <2:Kevin>>, <Employee: <5:Jack>>, <Employee: <6:Robin>>]>"

# 2、exclude(**kwargs)
	2.1: 有参,参数为过滤条件
	2.2: 返回值为QuerySet对象,QuerySet对象中包含了不符合过滤条件的多个记录对象
	queryset_res=Employee.objects.exclude(department='技术部')

# 3、all()
	3.1:无参
	3.2:返回值为QuerySet对象,QuerySet对象中包含了查询出的所有记录对象
	queryset_res = Employee.objects.all() # 查询出表中所有的记录对象

# 4、order_by(*field):
	4.1:有参,参数为排序字段,可以指定多个字段,在字段1相同的情况下,可以按照字段2进行排序,以此类推,默认升序排列,在字段前加横杆代表降序排(如"-id"	4.2:返回值为QuerySet对象,QuerySet对象中包含了排序好的记录对象
	queryset_res = Employee.objects.order_by("salary","-id") 
	"先按照salary字段升序排,如果salary相同则按照id字段降序排"

# 5、values(*field)
	5.1:有参,参数为字段名,可以指定多个字段
	5.2:返回值为QuerySet对象,QuerySet对象中包含的并不是一个个的记录对象,而上多个字典,字典的key即我们传入的字段名
        queryset_res = Employee.objects.values('id','name')
        print(queryset_res) # 输出:<QuerySet [{'id': 1, 'name': 'Egon'}, {'id': 2, 'name': 'Kevin'}, ......]>
        print(queryset_res[0]['name']) # 输出:Egon

# 6、values_list(*field):
	6.1:有参,参数为字段名,可以指定多个字段
	6.2:返回值为QuerySet对象,QuerySet对象中包含的并不是一个个的记录对象,而上多个小元组,字典的key即我们传入的字段名
        queryset_res = Employee.objects.values_list('id','name')
        print(queryset_res) # 输出:<QuerySet [(1, 'Egon'), (2, 'Kevin'),), ......]>
        print(queryset_res[0][1]) # 输出:Egon

Part3:

​ Part2中所示查询API的返回值都是QuerySet类型的对象,QuerySet类型是django ORM自定义的一种数据类型,专门用来存放查询出的多个记录对象,该类型的特殊之处在于
1、queryset类型类似于python中的列表,支持索引操作

"过滤出符合条件的多个记录对象,然后存放到QuerySet对象中"
queryset_res=Employee.objects.filter(department='技术部') 

"按照索引从QuerySet对象中取出第一个记录对象"
obj=queryset_res[0]
print(obj.name,obj.birth,obj.salary)

2、管理器objects下的方法queryset下同样可以调用,并且django的ORM支持链式操作,于是我们可以像下面这样使用

# 简单示范:
res=Employee.objects.filter(gender=1).order_by('-id').values_list('id','name')
print(res) 	"输出:<QuerySet [(6, 'Robin'), (5, 'Jack'), (4, 'Tom'), (2, 'Kevin')]>"

Part4:

其他查询API

# 1、reverse():
	1.1:无参
	1.2:对排序的结果取反,返回值为QuerySet对象
	queryset_res = Employee.objects.order_by("salary", "-id").reverse()

# 2、exists():
	2.1:无参
	2.2:返回值为布尔值,如果QuerySet包含数据,就返回True,否则返回False
        res = Employee.objects.filter(id=100).exists()
        print(res)  # 输出:False

# 3、distinct():
        3.1:如果使用的是Mysql数据库,那么distinct()无需传入任何参数
        3.2:从values或values_list的返回结果中剔除重复的记录对象,返回值为QuerySet对象
        res = Employee.objects.filter(name='Egon').values('name', 'salary').distinct()
        print(res) 		"输出:<QuerySet [{'name': 'Egon', 'salary': Decimal('100.1')}]>"

        res1 = Employee.objects.filter(name='Egon').values_list('name', 'salary').distinct()
        print(res1) 	"输出:<QuerySet [('Egon', Decimal('100.1'))]>"

2.3.2 基于双下划线的模糊查询#

插图4

2.3.3 F与Q查询#

F查询

在上面所有的例子中,我们在进行条件过滤时,都只是用某个字段与某个具体的值做比较。如果我们要对两个字段的值做比较,那该怎么做呢?

Django 提供 F() 来做这样的比较。F() 的实例可以在查询中引用字段,来比较两个不同字段的值,如下

# 一张书籍表中包含字段:评论数commentNum、收藏数keepNum,要求查询:评论数大于收藏数的书籍
from django.db.models import F
Book.objects.filter(commnetNum__lt=F('keepNum'))

Django 支持 F() 对象之间以及 F() 对象和常数之间的加减乘除和取模的操作

# 查询评论数大于收藏数2倍的书籍
from django.db.models import F
Book.objects.filter(commnetNum__lt=F('keepNum')*2)

修改操作也可以使用F函数,比如将每一本书的价格提高30元:

Book.objects.all().update(price=F("price")+30) 

更新字段等于字段本身再拼接一个字符串应该这样做

from django.db.models.functions import Concat
from django.db.models import Value

Employee.objects.filter(nid__lte=3).update(name=Concat(F('name'),Value('_sb')))

Q查询

img

filter() 等方法中逗号分隔开的多个关键字参数都是逻辑与(AND) 的关系。 如果我们需要使用逻辑或(OR)来连接多个条件,就用到了Django的Q对象

可以将条件传给类Q来实例化出一个对象,Q的对象可以使用&| 操作符组合起来,&等同于and,|等同于or

from django.db.models import Q
Employee.objects.filter(Q(id__gt=5) | Q(name="Egon"))

# 等同于sql:select * from app01_employee where id < 5 or name = 'Egon';

Q 对象可以使用~ 操作符取反,相当于NOT

from django.db.models import Q
Employee.objects.filter(~Q(id__gt=5) | Q(name="Egon"))

# 等同于sql:select * from app01_employee where not (id < 5) or name = 'Egon';

当我们的过滤条件中既有or又有and,则需要混用Q对象与关键字参数,但Q 对象必须位于所有关键字参数的前面

from django.db.models import Q
Employee.objects.filter(Q(id__gt=5) | Q(name="Egon"),salary__lt=100)

# 等同于sql:select * from app01_employee where (id < 5 or name = 'Egon') and salary < 100;

2.3.4 聚合查询#

​ 聚合查询aggregate()是把所有查询出的记录对象整体当做一个组,我们可以搭配聚合函数来对整体进行一个聚合操作

from django.db.models import Avg, Max, Sum, Min, Max, Count 	# 导入聚合函数

1. 调用objects下的aggregate()方法,会把表中所有记录对象整体当做一组进行聚合
    res1=Employee.objects.aggregate(Avg("salary")) 	select avg(salary) as salary__avg from app01_employee;
    print(res1) 	# 输出:{'salary__avg': 70.73}

2、aggregate()会把QuerySet对象中包含的所有记录对象当成一组进行聚合
    res2=Employee.objects.all().aggregate(Avg("salary")) 	
    "select avg(salary) as salary__avg from app01_employee;"
    print(res2) # 输出:{'salary__avg': 70.73}

    res3=Employee.objects.filter(id__gt=3).aggregate(Avg("salary")) 
    "select avg(salary) as salary__avg from app01_employee where id > 3;"
    print(res3) # 输出:{'salary__avg': 71.0}

aggregate()的返回值为字典类型,字典的key是由”聚合字段的名称___聚合函数的名称”合成的,例如

Avg("salary") 合成的名字为 'salary__avg'

若我们想定制字典的key名,我们可以指定关键参数,如下

res1=Employee.objects.all().aggregate(avg_sal=Avg('salary')) 	
"select avg(salary) as avg_sal from app01_employee;"

print(res1) 	"输出:{'avg_sal': 70.73} 	关键字参数名就会被当做字典的key"

如果我们想得到多个聚合结果,那就需要为aggregate传入多个参数

res1=Employee.objects.all().aggregate(nums=Count('id'),avg_sal=Avg('salary'),max_sal=Max('salary')) 
"相当于SQL:select count(id) as nums,avg(salary) as avg_sal,max(salary) as max_sal from app01_employee;"

print(res1) 	"输出:{'nums': 10, 'avg_sal': 70.73, 'max_sal': Decimal('200.3')}"

2.3.5 分组查询#

分组查询annotate()相当于sql语句中的group by,是在分组后,对每个组进行单独的聚合,需要强调的是,在进行单表查询时,annotate()必须搭配values()使用:values("分组字段").annotate(聚合函数),如下

# 表中记录
mysql> select * from app01_employee;
+----+-------+--------+------------+------------+--------+
| id | name  | gender | birth      | department | salary |
+----+-------+--------+------------+------------+--------+
|  1 | Egon  |      0 | 1997-01-27 | 财务部     |  100.1 |
|  2 | Kevin |      1 | 1998-02-27 | 技术部     |   10.1 |
|  3 | Lili  |      0 | 1990-02-27 | 运营部     |   20.1 |
|  4 | Tom   |      1 | 1991-02-27 | 运营部     |   30.1 |
|  5 | Jack  |      1 | 1992-02-27 | 技术部     |   11.2 |
|  6 | Robin |      1 | 1988-02-27 | 技术部     |  200.3 |
|  7 | Rose  |      0 | 1989-02-27 | 财务部     |   35.1 |
+----+-------+--------+------------+------------+--------+

# 查询每个部门下的员工数
res=Employee.objects.values('department').annotate(num=Count('id')) 
"相当于sql:
select department,count(id) as num from app01_employee group by department;"

print(res) 
"输出:<QuerySet [{'department': '财务部', 'num': 2}, {'department': '技术部', 'num': 3}, {'department': '运营部', 'num': 2}]>"

跟在annotate前的values方法,是用来指定分组字段,即group by后的字段,而跟在annotate后的values方法,则是用来指定分组后要查询的字段,即select 后跟的字段

res=Employee.objects.values('department').annotate(num=Count('id')).values('num')
"相当于sql:
select count(id) as num from app01_employee group by department;"

print(res)
"输出:<QuerySet [{'num': 2}, {'num': 3}, {'num': 2}]>"

跟在annotate前的filter方法表示where条件,跟在annotate后的filter方法表示having条件,如下

# 查询男员工数超过2人的部门名
res=Employee.objects.filter(gender=1).values('department').annotate(male_count=Count("id")).filter(male_count__gt=2).values('department')

print(res) 	"输出:<QuerySet [{'department': '技术部'}]>"

"解析:
1、跟在annotate前的filter(gender=1) 相当于 where gender = 1,先过滤出所有男员工信息
2、values('department').annotate(male_count=Count("id")) 相当于group by department,对过滤出的男员工按照部门分组,然后聚合出每个部门内的男员工数赋值给字段male_count
3、跟在annotate后的filter(male_count__gt=2) 相当于 having male_count > 2,会过滤出男员工数超过2人的部门
4、最后的values('department')代表从最终的结果中只取部门名"

总结:

1、values()在annotate()前表示group by的字段,在后表示取值
1filter()在annotate()前表示where条件,在后表示having

需要注意的是,如果我们在annotate前没有指定values(),那默认用表中的id字段作为分组依据,而id各不相同,如此分组是没有意义的,如下

res=Employee.objects.annotate(Count('name')) # 每条记录都是一个分组
res=Employee.objects.all().annotate(Count('name')) # 同上

2.4 修改记录#

2.4.1 直接修改单条记录#

可以修改记录对象属性的值,然后执行save方法从而完成对单条记录的直接修改

# 1、获取记录对象
obj=Employee.objects.filter(name='Egon')[0]
# 2、修改记录对象属性的值
obj.name='EGON'
obj.gender=1
# 3、重新保存
obj.save()

2.4.2 修改QuerySet中的所有记录对象#

QuerySet对象下的update()方法可以更QuerySet中包含的所有对象,该方法会返回一个整型数值,表示受影响的记录条数(相当于sql语句执行结果的rows)

queryset_obj=Employee.objects.filter(id__gt=5)
rows=queryset_obj.update(name='EGON',gender=1)

2.5 删除记录#

2.5.1 直接删除单条记录#

可以直接调用记录对象下的delete方法,该方法运行时立即删除本条记录而不返回任何值,如下

obj=Employee.objects.first()
obj.delete()

2.5.2 删除QuerySet中的所有记录对象#

每个 QuerySet下也都有一个 delete() 方法,它一次性删除 QuerySet 中所有的对象(如果QuerySet对象中只有一个记录对象,那也就只删一条),如下

queryset_obj=Employee.objects.filter(id__gt=5)
rows=queryset_obj.delete()
需要强调的是管理objects下并没有delete方法,这是一种保护机制,是为了避免意外地调用 Employee.objects.delete() 
方法导致所有的记录被误删除从而跑路。但如果你确认要删除所有的记录,那么你必须显式地调用管理器下的all方法,拿
到一个QuerySet对象后才能调用delete方法删除所有
Employee.objects.all().delete()

3 总结#

模板:models.表名.object.操作

3.1 单表操作#

单表操作
"django自带的sqlite3数据库对日期格式不是很敏感,处理的时候容易出错"
    res = models.User.objects.create(name='jason',age=18,register_time='2002-1-21')
    print(res)
    import datetime
    ctime = datetime.datetime.now()
    user_obj = models.User(name='egon',age=84,register_time=ctime)
    user_obj.save()

    res = models.User.objects.filter(pk=2).delete()
    print(res)
    """
    pk会自动查找到当前表的主键字段,指代的就是当前表的主键字段
    用了pk之后,你就不需要指代当前表的主键字段到底叫什么了,管他叫什么id、uid...
    """
    user_obj = models.User.objects.filter(pk=1).first()
    user_obj.delete()

修改
    models.User.objects.filter(pk=4).update(name='egonDSB')

    user_obj = models.User.objects.get(pk=4)
    user_obj = models.User.objects.filter(pk=6)
    user_obj.name = 'egonPPP'
    user_obj.save()
    """
    get方法返回的直接就是当前数据对象,但是该方法不推荐使用,一旦数据不存在该方法会直接报错
    而filter则不会,所以我们还是用filter
    """

3.2 必知必会13条#

必知必会13条
必知必会13    1.all()  查询所有数据

    2.filter()     带有过滤条件的查询
    
    3.get()        直接拿数据对象 但是条件不存在直接报错
    
    4.first()      拿queryset里面第一个元素
    res = models.User.objects.all().first()
    print(res)
    
    5.last()
    res = models.User.objects.all().last()
    print(res)

    6.values()  可以指定获取的数据字段  select name,age from ...     结果是列表套字典
    res = models.User.objects.values('name','age')  
    print(res)
    "<QuerySet [{'name': 'jason', 'age': 18}, {'name': 'egonPPP', 'age': 84}]>"
    
    7.values_list()  结果是列表套元祖
    res = models.User.objects.values_list('name','age')  
    print(res)
    "<QuerySet [('jason', 18), ('egonPPP', 84)]>"
    """
     查看内部封装的sql语句
     上述查看sql语句的方式,只能用于queryset对象,只有queryset对象才能够点击query查看内部的sql语句
    """
    
    8.distinct()  去重
    res = models.User.objects.values('name','age').distinct()
    print(res)
    """
    去重一定要是一模一样的数据,如果带有主键那么肯定不一样 你在往后的查询中一定不要忽略主键
    """
    
    9.order_by()
    res = models.User.objects.order_by('age')  默认升序
    res = models.User.objects.order_by('-age')  降序
    print(res)
    
    10.reverse()  反转的前提是:数据已经排过序了  order_by()
    res = models.User.objects.all()
    res1 = models.User.objects.order_by('age').reverse()
    print(res,res1)

    11.count()  统计当前数据的个数
    res = models.User.objects.count()
    print(res)
    
    12.exclude()  排除在外
    res = models.User.objects.exclude(name='jason')
    print(res)

    13.exists()  基本用不到因为数据本身就自带布尔值  返回的是布尔值
    res = models.User.objects.filter(pk=10).exists()
    print(res)

3.3 查看内部SQL语句的方式#

查看内部SQL语句的方式
方式1
res = models.User.objects.values_list('name','age')  "<QuerySet [('jason', 18), ('egonPPP', 84)]>"
print(res.query)
queryset对象才能够点击query查看内部的sql语句

方式2:所有的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.4 测试脚本#

测试脚本
"""
当你只是想测试django中的某一个py文件内容时,那么你可以不用书写前后端交互的形式,而是直接写一个测试脚本即可
脚本代码无论是写在应用下的tests.py还是自己单独开设py文件都可以
"""
测试环境的准备:去manage.py中拷贝前四行代码,然后自己写两行。
在这个代码块的下面就可以测试django里面的单个py文件了
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "day64.settings")
    import django
    django.setup()

3.5 神奇的双下划綫查询#

神奇的双下划线查询
       年龄大于35岁的数据
        res = models.User.objects.filter(age__gt=35)
        print(res)

        年龄小于35岁的数据
        res = models.User.objects.filter(age__lt=35)
        print(res)
        
        大于等于 
        res = models.User.objects.filter(age__gte=32)
        print(res)
        
        小于等于
        res = models.User.objects.filter(age__lte=32)
        print(res)

        年龄是18 或者 32 或者40
        res = models.User.objects.filter(age__in=[18,32,40])
        print(res)

        年龄在1840岁之间的,首尾都要
        res = models.User.objects.filter(age__range=[18,40])
        print(res)

        查询出名字里面含有s的数据,模糊查询
        res = models.User.objects.filter(name__contains='s')
        print(res)
        
        查询出名字里面含有p的数据是区分大小写
        res = models.User.objects.filter(name__contains='p')
        print(res)
        
        如果想要忽略大小写
        res = models.User.objects.filter(name__icontains='p')
        print(res)
	
	以什么开始或结束
        res = models.User.objects.filter(name__startswith='j')
        res1 = models.User.objects.filter(name__endswith='j')

	时间条件,查询出注册时间是 2020 1        res = models.User.objects.filter(register_time__month='1')
        res = models.User.objects.filter(register_time__year='2020')

3.6 多对一外键增删改查#

多对一外键增删改查
   1  直接写实际字段 id
   models.Book.objects.create(title='论语',price=899.23,publish_id=1)
   models.Book.objects.create(title='聊斋',price=444.23,publish_id=2)
   models.Book.objects.create(title='老子',price=333.66,publish_id=1)
   2  虚拟字段,对象
   publish_obj = models.Publish.objects.filter(pk=2).first()
   models.Book.objects.create(title='红楼梦',price=666.23,publish=publish_obj)

   models.Publish.objects.filter(pk=1).delete() 级联删除

修改
   models.Book.objects.filter(pk=1).update(publish_id=2)
   publish_obj = models.Publish.objects.filter(pk=1).first()
   models.Book.objects.filter(pk=1).update(publish=publish_obj)

3.7 多对多外键增删改查#

多对多外键增删改查
如何给书籍添加作者?
    book_obj = models.Book.objects.filter(pk=1).first()
    print(book_obj.authors)  	用点的方式,这时就类似于你已经到了第三张关系表了
    book_obj.authors.add(1)  	这时将书籍id1的书籍绑定一个主键为1 的作者
    book_obj.authors.add(2,3)

    author_obj = models.Author.objects.filter(pk=1).first()
    author_obj1 = models.Author.objects.filter(pk=2).first()
    author_obj2 = models.Author.objects.filter(pk=3).first()
    book_obj.authors.add(author_obj)
    book_obj.authors.add(author_obj1,author_obj2)
    """
    add给第三张关系表添加数据,括号内既可以传数字也可以传对象 并且都支持多个
    """

    book_obj.authors.remove(2)
    book_obj.authors.remove(1,3)

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


修改
    book_obj.authors.set([1,2])  括号内必须给一个可迭代对象
    book_obj.authors.set([3])    括号内必须给一个可迭代对象

    author_obj = models.Author.objects.filter(pk=2).first()
    author_obj1 = models.Author.objects.filter(pk=3).first()
    book_obj.authors.set([author_obj,author_obj1])  括号内必须给一个可迭代对象

    """
    set括号内必须传一个可迭代对象,该对象内既可以数字也可以对象 并且都支持多个
    """


清空
    在第三张关系表中清空某个书籍与作者的绑定关系
    book_obj.authors.clear()
    """
    clear括号内不要加任何参数
    """

3.8 正反向概念#

正反向概念
概念解释
	外键字段在我手上那么,我查你就是正向
	外键字段如果不在手上,我查你就是反向

关键口诀
    正向查询按字段
    反向查询按表名小写

3.9 聚合查询#

聚合查询
"""
    聚合查询需要用到:aggregate
    聚合查询通常情况下都是配合分组一起使用的
    只要是跟数据库相关的模块 
        基本上都在django.db.models里面
        如果上述没有那么应该在django.db里面
"""
    from app01 import models
    from django.db.models import Max,Min,Sum,Count,Avg
    
1 所有书的平均价格
    res = models.Book.objects.aggregate(Avg('price'))
    print(res)
    
2.上述方法一次性使用
    res = models.Book.objects.aggregate(Max('price'),Min('price'),Sum('price'),Count('pk'),Avg('price'))
    print(res)

3.10 分组查询#

分组查询
"""
    分组查询需要用:annotate
    MySQL分组查询都有哪些特点:分组之后默认只能获取到分组的依据,组内其他字段都无法直接获取了,这是因为严格模式
    ONLY_FULL_GROUP_BY    
"""
    from django.db.models import Max, Min, Sum, Count, Avg
1.统计每一本书的作者个数
    res = models.Book.objects.annotate()  "models后面点什么,就是按什么分组,这里按照了书分组"
    res = models.Book.objects.annotate(author_num=Count('authors')).values('title','author_num')
    """
    这里按照书分组后,我们自己定义了author_num字段,用来存储统计出来的每本书对应的作者个数,然后通过value取值
    """
    res1 = models.Book.objects.annotate(author_num=Count('authors__id')).values('title','author_num')
    print(res,res1)


2.统计每个出版社卖的最便宜的书的价格(作业:复习原生SQL语句 写出来)
    res = models.Publish.objects.annotate(min_price=Min('book__price')).values('name','min_price')
    print(res)

3.统计不止一个作者的图书
        1.先按照图书分组,求每一本书对应的作者个数
        2.过滤出不止一个作者的图书
    res = models.Book.objects.annotate(author_num=Count('authors')).filter(author_num__gt=1).values('title','author_num')
    """
    只要你的orm语句得出的结果还是一个queryset对象,那么它就可以继续无限制的点queryset对象封装的方法
    """
    print(res)

    4.查询每个作者出的书的总价格
    res = models.Author.objects.annotate(sum_price=Sum('book__price')).values('name','sum_price')
    print(res)

    """
    如果我想按照指定的字段分组该如何处理呢?
        models.Book.objects.values('price').annotate()
    
    如果机器上如果出现分组查询报错的情况,很有可能是你需要修改数据库严格模式
    """

3.11 F与Q查询#

F查询
1.查询卖出数大于库存数的书籍
    """
    F查询:能够帮助你直接获取到表中某个字段对应的数据
    """
    from django.db.models import F
    res = models.Book.objects.filter(maichu__gt=F('kucun'))
    print(res)


2.将所有书籍的价格提升500    models.Book.objects.update(price=F('price') + 500)


3.将所有书的名称后面加上爆款两个字
    """
    在操作字符类型的数据的时候,F不能够直接做到字符串的拼接
    """
    from django.db.models.functions import Concat
    from django.db.models import Value
    models.Book.objects.update(title=Concat(F('title'), Value('爆款')))
    models.Book.objects.update(title=F('title') + '爆款')  所有的名称会全部变成空白
Q查询
 1.查询卖出数大于100或者价格小于600的书籍
    res = models.Book.objects.filter(maichu__gt=100,price__lt=600)
    "filter括号内多个参数是and关系,而Q查询能修改两个条件之间的"
    from django.db.models import Q
    res = models.Book.objects.filter(Q(maichu__gt=100),Q(price__lt=600))  Q包裹逗号分割 还是and关系
    res = models.Book.objects.filter(Q(maichu__gt=100)|Q(price__lt=600))  | or关系
    res = models.Book.objects.filter(~Q(maichu__gt=100)|Q(price__lt=600))  ~ not关系
    print(res)  <QuerySet []>

2.Q的高阶用法  能够将查询条件的左边也变成字符串的形式
    q = Q()
    q.connector = 'or'
    q.children.append(('maichu__gt',100))
    q.children.append(('price__lt',600))
    res = models.Book.objects.filter(q)  默认还是and关系
    print(res)

3.12 django中如何开启事务#

点击查看代码
"""
事务
	ACID
		原子性
			不可分割的最小单位
		一致性
			跟原子性是相辅相成
		隔离性
			事务之间互相不干扰
		持久性
			事务一旦确认永久生效
	
	事务的回滚 
		rollback
	事务的确认
		commit
"""
目前你只需要掌握Django中如何简单的开启事务

    from django.db import transaction
    try:
        with transaction.atomic():
            sql1
            sql2
            ...
with代码快内书写的所有orm操作都是属于同一个事务
    except Exception as e:
        print(e)
    print('执行其他操作')

3.13 数据库查询优化#

点击查看代码
"""
1.only与defer	

2.select_related与prefetch_related

3.优化的是orm语句的特点:惰性查询
	如果你仅仅只是书写了orm语句,在后面根本没有用到该语句所查询出来的参数,那么orm会自动识别 直接不执行
"""

------------------only------------------
    res = models.Book.objects.all()
    print(res)  "要用数据了才会走数据库"

"想要获取书籍表中所有数的名字"
    res = models.Book.objects.values('title')
    for d in res:
        print(d.get('title'))
        
"你给我实现获取到的是一个数据对象,然后点title就能够拿到书名,并且没有其他字段"
    res = models.Book.objects.only('title')
    res = models.Book.objects.all()
    print(res)  
"<QuerySet [<Book: 三国演义爆款>, <Book: 红楼梦爆款>, <Book: 论语爆款>, <Book: 聊斋爆款>, <Book: 老子爆款>]>"
    for i in res:
        print(i.title)  "点击only括号内的字段,不会走数据库"
        print(i.price)  "点击only括号内没有的字段,会重新走数据库查询而all不需要走了"

        
------------------defer------------------
    res = models.Book.objects.defer('title')  "对象除了没有title属性之外其他的都有"
    for i in res:
        print(i.price)
    """
    defer与only刚好相反
        defer括号内放的字段不在查询出来的对象里面,查询该字段需要重新走数据
        而如果查询的是非括号内的字段,则不需要走数据库了

    """
    

------------------select_related 跟跨表操作有关------------------
    res = models.Book.objects.all()
    for i in res:
        print(i.publish.name)  "每循环一次就要走一次数据库查询"

    res = models.Book.objects.select_related('authors')  "相当于MySQL中的INNER JOIN"
    """
    select_related内部直接先将book与publish连起来,然后一次性将大表里面的所有数据全部封装给查询出来的对象
    这个时候对象无论是点击book表的数据还是publish的数据都无需再走数据库查询了
    
    select_related括号内只能放外键字段:一对多、 一对一
    多对多也不行
    """
------------------prefetch_related 跟跨表操作有关------------------
    for i in res:
        print(i.publish.name)  "每循环一次就要走一次数据库查询"

    res = models.Book.objects.prefetch_related('publish')  "相当于MySQL中的子查询"
    """
    prefetch_related该方法内部其实就是子查询
    将子查询查询出来的所有结果也给你封装到对象中,给你的感觉好像也是一次性搞定的
    """
    for i in res:
        print(i.publish.name)

3.14 choices参数(数据库字段设计常见)#

点击查看代码
"""
用户表	
	性别
	学历
	工作经验
	是否结婚
	是否生子
	客户来源
	...
针对某个可以列举完全的可能性字段,我们应该如何存储

只要某个字段的可能性是可以列举完全的,那么一般情况下都会采用choices参数
"""
class User(models.Model):
        username = models.CharField(max_length=32)
        age = models.IntegerField()
        # 性别
        gender_choices = (
            (1,'男'),
            (2,'女'),
            (3,'其他'),
        )
        gender = models.IntegerField(choices=gender_choices)

        score_choices = (
            ('A','优秀'),
            ('B','良好'),
            ('C','及格'),
            ('D','不合格'),
        )
        # 保证字段类型跟列举出来的元祖第一个数据类型一致即可
        score = models.CharField(choices=score_choices,null=True)
        """
        该gender字段存的还是数字 但是如果存的数字在上面元祖列举的范围之内
        那么可以非常轻松的获取到数字对应的真正的内容

        1.gender字段存的数字不在上述元祖列举的范围内容
        2.如果在 如何获取对应的中文信息
        """
    
    
    from app01 import models
    models.User.objects.create(username='jason',age=18,gender=1)
    models.User.objects.create(username='egon',age=85,gender=2)
    models.User.objects.create(username='tank',age=40,gender=3)
    "存的时候,没有列举出来的数字也能存(范围还是按照字段类型决定)"
    models.User.objects.create(username='tony',age=45,gender=4)

    user_obj = models.User.objects.filter(pk=1).first()
    print(user_obj.gender)
    "只要是choices参数的字段,如果你想要获取对应信息,固定写法:get_字段名_display()"
    print(user_obj.get_gender_display())

    user_obj = models.User.objects.filter(pk=4).first()
   " 如果没有对应关系那么字段是什么还是展示什么"
    print(user_obj.get_gender_display())  "4"

3.15 MTV与MVC模型#

点击查看代码
# MTV:Django号称是MTV模型
M:models
T:templates
V:views
# MVC:其实django本质也是MVC
M:models
V:views
C:controller
  
# vue框架:MVVM模型

3.16 多对多三种创建方式#

点击查看代码
全自动:利用orm自动帮我们创建第三张关系表
	class Book(models.Model):
    name = models.CharField(max_length=32)
    authors = models.ManyToManyField(to='Author')
	class Author(models.Model):
    name = models.CharField(max_length=32)
	"""
	优点:代码不需要你写,非常的方便,还支持orm提供操作第三张关系表的方法...
	不足之处:第三张关系表的扩展性极差(没有办法额外添加字段...)
	"""
    
    
纯手动
	class Book(models.Model):
    name = models.CharField(max_length=32)
    
	class Author(models.Model):
    name = models.CharField(max_length=32)
  
  class Book2Author(models.Model):
    book_id = models.ForeignKey(to='Book')
    author_id = models.ForeignKey(to='Author')
  '''
  优点:第三张表完全取决于你自己进行额外的扩展
  不足之处:需要写的代码较多,不能够再使用orm提供的简单的方法
  不建议你用该方式
  '''


半自动
class Book(models.Model):
    name = models.CharField(max_length=32)
    authors = models.ManyToManyField(to='Author',
                                     through='Book2Author',
                                     through_fields=('book','author')
                                     )
    
    
class Author(models.Model):
    name = models.CharField(max_length=32)
""" 外键设置在第二张表时
    books = models.ManyToManyField(to='Book',
                                     through='Book2Author',
                                     through_fields=('author','book')
                                     )"""
    
    
class Book2Author(models.Model):
    book = models.ForeignKey(to='Book')
    author = models.ForeignKey(to='Author')

"""
through_fields字段先后顺序
    判断的本质:
        通过第三张表查询对应的表,需要用到哪个字段,就把哪个字段放前面
    你也可以简化判断
        当前表是谁,就把对应的关联字段放前面
        
        
半自动:可以使用orm的正反向查询,但是没法使用add、set、remove、clear这四个方法
"""

# 总结:你需要掌握的是全自动和半自动,为了扩展性更高,一般我们都会采用半自动(写代码要给自己留一条后路)

3.17 Ajax#

朝发送请求的方式 请求类型
1.浏览器地址栏直接输入url回车 GET请求
2.a标签href属性 GET请求
3.form表单 GET请求/POST请求
4.ajax GET请求/POST请求
Ajax特点:
	异步提交
	局部刷新

	AJAX(Asynchronous Javascript And XML)翻译成中文就是“异步的Javascript和XML”。即使用Javascript语言与服务器进行异步交互,传输的数据为XML(当然,传输的数据不只是XML)。

	AJAX 不是新的编程语言,而是一种使用现有标准的新方法。

	AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。(这一特点给用户的感受是在不知不觉中完成请求和响应过程)

	AJAX 不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。

                - 同步交互:客户端发出一个请求后,需要等待服务器响应结束后,才能发出第二个请求;
                - 异步交互:客户端发出一个请求后,无需等待服务器响应结束,就可以发出第二个请求。

	Ajax我们只学习jQuery封装之后的版本(不学原生的 原生的复杂并且在实际项目中也一般不用),所以我们在前端页面使用ajax的时候需要确保导入了jQuery
	并不只有jQuery能够实现ajax,其他的框架也可以 但是换汤不换药 原理是一样的

应用示例

应用:1

页面上有三个input	在前两个框中输入数字 点击按钮 朝后端发送ajax请求
	后端计算出结果 再返回给前端动态展示的到第三个input框中
	(整个过程页面不准有刷新,也不能在前端计算)
视图函数
from django.shortcuts import render,HttpResponse

import json
from django.http import JsonResponse

def ab_ajax(request):
        if request.method == "POST":

        """    print(request.POST)  <QueryDict: {'username': ['jason'], 'password': ['123']}>
                i1 = request.POST.get('i1')
                i2 = request.POST.get('i2')
                先转成整型再加
                i3 = int(i1) + int(i2)
                print(i3)"""

                d = {'code':100,'msg':i3}
                d = {'code':100,'msg':666}
                "return HttpResponse(json.dumps(d))"
                return JsonResponse(d)  "当有Ajax请求时,视图函数的返回值就不会直接返回给浏览器了,而是返回给HTML页面中处理Ajax请求的函数"
    
    return render(request,'index.html')  "首先给浏览器返回一个页面"
HTML页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>

<body>
    <input type="text" id="d1"> +
    <input type="text" id="d2"> =
    <input type="text" id="d3">
    
    <p>
        <button id="btn">点我</button>
    </p>

    <script>
            // 先给按钮绑定一个点击事件
            $('#btn').click(function () {
                    // 朝后端发送ajax请求
                    $.ajax({
                            // 1.指定朝哪个后端发送ajax请求
                            url:'', // 不写就是朝当前地址提交
                        
                            // 2.请求方式
                            type:'post',  // 不指定默认就是get 都是小写
                        
                            // 3.数据
                            "{data:{'username':'jason','password':123},}"
                            data:{'i1':$('#d1').val(),'i2':$('#d2').val()},
                                
                            // 4.回调函数:当后端给你返回结果的时候会自动触发,args接受后端的返回结果
                            success:function (args) {
                                    {alert(args)  // 通过DOM操作动态渲染到第三个input里面#}
                                    {$('#d3').val(args)}
                                    console.log(typeof args)
                                    
                            }
                    })
            })
    </script>
</body>
</html>
                
         
"""
针对后端如果是用HttpResponse返回的数据,回调函数不会自动帮你反序列化
如果后端直接用的是JsonResponse返回的数据,回调函数会自动帮你反序列化

HttpResponse解决方式
	1.自己在前端利用JSON.parse()
	2.在ajax里面配置一个参数
			(后面再讲)
"""

应用:2

HTML代码部分
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<input type="text" id="i1"> + <input type="text" id="i2"> = <input type="text" id="i3">
<button id="b1">Ajax Test</button>

<script src="/static/jquery-3.3.1.min.js"></script>

<script>
        $('#b1').click(function () {
                $.ajax({
                        url:'',
                        type:'POST',
                        data:{i1:$('#i1').val(),i2:$('#i2').val()},
                        success:function (data) {
                                $('#i3').val(data)
                    }
                })
        })

</script>
</body>
</html>
views.py
def ajax_test(request):
    if request.method=='POST':
        i1=request.POST.get('i1')
        i2=request.POST.get('i2')
        ret=int(i1)+int(i2)
        return HttpResponse(ret)
    return render(request,'ajax_test.html')
url.py
from django.conf.urls import url
from app01 import views
urlpatterns=[
    url(r'^ajax_test/',views.ajax_test),
]
posted @   maju  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
点击右上角即可分享
微信分享提示
主题色彩