flask-sqlalchemy

1.sqlalchemy快速使用

flask中没有orm框架。我们需要使用一个对象关系映射来操作数据库。sqlalchemy就是其中之一。
SQLAlchemy是一个基于Python实现的ORM框架。该框架建立在 DB API之上,使用关系对象映射进行数据库操作,简言之便是:将类和对象转换成SQL,然后使用数据API执行SQL并获取执行结果
SQLAlchemy本身无法操作数据库,其必须以来pymsql等第三方插件
安装:

pip3.10 install sqlalchemy

补充:SQLAlchemy本身无法操作数据库,其必须以来pymsql等第三方插件

mysql+pymysql://<username>:<password>@<host>/<dbname>[?<options>]

cx_Oracle
oracle+cx_oracle://user:pass@host:port/dbname[?key=value&key=value...]

# 更多:http://docs.sqlalchemy.org/en/latest/dialects/index.html

1.1 使用原生sql连接数据库

sqlalchemy自带连接池,所以不需要自己创建连接池

# 1.导入模块
import time
import threading
import sqlalchemy
from sqlalchemy import create_engine
from sqlalchemy.engine.base import Engine

# 生成引擎对象,sqlalchemy自带连接池
engine = create_engine(
    "mysql+pymysql://root:123456@127.0.0.1:3306/book?charset=utf8",
    max_overflow=0,  # 超过连接池大小外最多创建的连接
    pool_size=5,  # 连接池大小
    pool_timeout=30,  # 池中没有线程最多等待的时间,否则报错
    pool_recycle=-1  # 多久之后对线程池中的线程进行一次连接的回收(重置),-1是不回收
)

conn = engine.raw_connection()
cursor=conn.cursor()
cursor.execute('select * from book')
print(cursor.fetchall())

image

1.2 创建数据表

# 1.导入
from sqlalchemy import create_engine
import datetime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, UniqueConstraint, Index
import datetime

# 2.执行declarative_base,得到一个类
Base = declarative_base()


# 3.继承Base类
class User(Base):
    # 4.写字段
    id = Column(Integer, primary_key=True)  # 整形类型、主键
    name = Column(String(32), index=True, nullable=False)  # varchar类型,最大长度为32,主键索引,不可为空
    email = Column(String(32), unique=True)
    ctime = Column(DateTime, default=datetime.datetime.now)  # 这个字段可以不用传,自动生成当前时间
    extra = Column(Text, nullable=True)

    # 5.写表名
    __tablename__ = 'users'  # 不写表名默认以类名为表名

    # 6.建立联合索引,联合唯一
    __table_args__ = (
        UniqueConstraint('id', 'name', name='uix_id_name'),  # 联合唯一
        Index('ix_id_name', 'name', 'email'),  # 索引
    )
	'''控制单个数据返回数据的name字段'''
	def __str__(self):
        return self.name
	'''控制容器(列表)中的数据返回每个数据中的name字段'''
    def __repr__(self):
        return self.name


class Book(Base):
    __tablename__ = 'books'
    id = Column(Integer, primary_key=True)
    name = Column(String(32))


# 7.将表同步到数据库中,不会创建库,库需要自己创建,只会创建表
engine = create_engine(
    "mysql+pymysql://root:123456@127.0.0.1:3306/user",  # 需要手动创建一个名为name的库
    max_overflow=0,  # 超过连接池大小外最多创建的连接
    pool_size=5,  # 连接池大小
    pool_timeout=30,  # 池中没有线程最多等待的时间,否则报错
    pool_recycle=-1  # 多久之后对线程池中的线程进行一次连接的回收(重置)
)

# 8.将所有被Base管理的表都同步到数据库
Base.metadata.create_all(engine)

# 9.删除掉Base管理的表
# Base.metadata.drop_all(engine)

image

2.sqlalchemy快速插入数据

使用orm插入:

from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from models import Book,User
# 1.生成engine对象
engine = create_engine(
    "mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8",
    max_overflow=0,  # 超过连接池大小外最多创建的连接
    pool_size=5,  # 连接池大小
    pool_timeout=30,  # 池中没有线程最多等待的时间,否则报错
    pool_recycle=-1  # 多久之后对线程池中的线程进行一次连接的回收(重置),-1是不回收
)

# 2.传入engine,拿到Session类
Session = sessionmaker(bind=engine)
# 3.拿到session对象
session = Session()
# 4.新增数据,主键是id可以不写
user = User(name='henry',email='2@qq.com')
session.add(user)
print(user)  # 是新产生的数据对象
#  5.一定要commit才能保存到数据库
session.commit()
session.close()

image

3.scoped_session线程安全

3.1 不使用scoped_session情况

再添加之前需要使用session对象,如果我们添加在全局,python多线程(实际上是同一条线程在程序之间切换)会造成数据的错乱,全局都用一个session会产生并发安全的问题,所以需要写在函数内:

from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from models import Book, User
from flask import Flask

engine = create_engine(
    "mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8",
    max_overflow=0,  # 超过连接池大小外最多创建的连接
    pool_size=5,  # 连接池大小
    pool_timeout=30,  # 池中没有线程最多等待的时间,否则报错
    pool_recycle=-1  # 多久之后对线程池中的线程进行一次连接的回收(重置),-1是不回收
)
app = Flask(__name__)
Session = sessionmaker(bind=engine)


@app.route('/index')
def index():
    book_obj = Book(name='红楼梦')
    '''为了防止数据错乱,将session对象放在函数内产生'''
    session = Session()
    session.add(book_obj)

    session.commit()
    session.close()
    return '添加成功'

if __name__ == '__main__':
    app.run()

3.2 使用scoped_session情况

之前我们是将每个session对象放在每个函数内,现在使用scoped_session可以将其放在全局

from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from models import Book, User
from flask import Flask
from sqlalchemy.orm import scoped_session
from threading import Thread

engine = create_engine(
    "mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8",
    max_overflow=0,  # 超过连接池大小外最多创建的连接
    pool_size=5,  # 连接池大小
    pool_timeout=30,  # 池中没有线程最多等待的时间,否则报错
    pool_recycle=-1  # 多久之后对线程池中的线程进行一次连接的回收(重置),-1是不回收
)
app = Flask(__name__)
Session = sessionmaker(bind=engine)

session = scoped_session(Session)

def task():
    book_obj = Book(name='西游记')
    session.add(book_obj)
    session.commit()
    session.close()
    return '增加成功'

for i in range(3):
    t = Thread(target=task)
    t.start()

image
那么scoped_session底层如何实现的呢?首先在代码

session = scoped_session(Session)

中,session是一个类scoped_session的对象,而不是原来的Session类对象了。这个类使用了一个类装饰器来讲属性注册在了这个类上,所以我们拿到了session对象可以有类Session的所有方法:
image

3.3 类装饰器

在类scoped_session中没有相应的方法,是通过类装饰器对其加上了方法。类装饰器是通过在装饰器中给类添加了属性和方法,对象也可以调用。和函数装饰器不同的是传入的参数是一个类,所以func就是类名Student,func()就是类Student的对象,通过对象点的方式给对象设置值:

def index():
    print('index执行了')

def wrapper(func):
    def inner(*args,**kwargs):
        res = func()
        res.age=22
        res.index=index
        return res
    return inner

@wrapper
class Student:
    name = 'max'

stu1 = Student()
print(stu1.name)
print(stu1.age)
stu1.index()

4.基本增删查改

1.增加有两个,add()和add_all()
add()括号内的参数是对象
add_all()括号内的参数是一个列表,里面可以放多个对象

from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from models import Book, User
from sqlalchemy.orm import scoped_session
'''engine其他参数有默认值,直接传地址用户名密码就可以'''
engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8",)

Session = sessionmaker(bind=engine)
session = scoped_session(Session)
user_obj = User(name='marry',email='123@qq.com')
user_obj1 = User(name='qqq',email='222@qq.com')
user_obj2 = User(name='www',email='111@qq.com')
# session.add(user_obj)
session.add_all([user_obj1,user_obj2])

session.commit()
session.close()

image
注:add_all()参数的列表中可以放多个模型表的对象,会把数据增加到不同的表里:

from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from models import Book, User
from sqlalchemy.orm import scoped_session

engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8",)

Session = sessionmaker(bind=engine)
session = scoped_session(Session)

user_obj = User(name='eee',email='333@qq.com')
book_obj = Book(name='水浒传')
session.add_all([user_obj,book_obj])
session.commit()
session.close()

image
2.基本查询:
session.query()括号内写表名,可以写多个表名

filter()括号内必须写表达式:也就是含有==、>、<、>=、<=、!=的式子
filter_by()括号内必须要写等式,只写一个等于号

all()和first():如果有多个结果需要加all(),返回结果是个列表,支持索引。只有一个结果时需要加first()

from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from models import Book, User
from sqlalchemy.orm import scoped_session

engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8",)

Session = sessionmaker(bind=engine)
session = scoped_session(Session)

user_obj = session.query(User).filter(User.name=='eee').first()
'''由于在表模型中定义了__str__,所以结果返回了用户的name'''
print(user_obj)  # eee

'''查询用户id大于等于9的用户'''
user_obj = session.query(User).filter(User.id >= 9).all()
'''由在表模型在定义了__repr__,所以列表中直接返回了每个用户的name'''
print(user_obj)  # [www, eee]
'''查询name不是eee的用户'''
user_obj = session.query(User).filter(User.name != 'eee').all()
print(user_obj)  # [max, henry, 小丽, 小红, marry, qqq, www]

'''filter_by需要跟等式'''
user_obj = session.query(User).filter_by(name='www').first()
print(user_obj)  # www


#  查询不需要做commit和close操作

3.删除:删除必须首先查到数据,才能改。删除只需要将查出来的数据直接点delete()即可,不需要点all()或者first(),返回值是影响的行数。增加、删除、修改都需要加session.commit(),只有查询不需要

res = session.query(User).filter(User.name=='eee').delete()
print(res)  # 1
session.commit()
session.close()

4.修改:修改有两种方式
方式一:直接查出数据对象,不点all()或者first()的情况下.update()中穿一个字典,会改掉相应的字段:

res = session.query(User).filter(User.name=='www').update({'name':'www1'})
session.commit()
session.close()

方式二:先查找到数据对象,在通过对象点字段名的方式修改,再用session.add()修改。session.add()内的对象如果能点到id的话,就执行修改操作;点不到则执行新增操作

user_obj = session.query(User).filter(User.name=='www1').first()
user_obj.name = 'www'
session.add(user_obj)

修改还可以直接在字段上进行修改:
在字符串类型的字段上进行拼接:

'''在id>6的用户对象中的name字段后面拼接字符串222'''
session.query(User).filter(User.id > 6).update({User.name:User.name+'222'},synchronize_session=False)

image
对整形字段上进行加减:
session.query(User).filter(User.age > 20).update({User.age:User.age+10},synchronize_session="evaluate")

5.高级查询(单表查询)

1.查询指定字段:在query参数中指定查询字段,label相当于原生sql中的起别名:

res = session.query(User.name.label('xxx'),User.email).all()
print(res)  # [('henry', '2@qq.com'), ('marry222', '123@qq.com')]

2.查询语句最后只要不点all()或者first(),那么执行的结果就是原生sql:

res = session.query(User.name.label('xxx'),User.email)
print(res)  # SELECT users.name AS xxx, users.email AS users_email FROM users

3.filter中可以跟多个条件,需要用逗号隔开。filter括号内跟的是表达式,filter_by括号内跟的是参数:

res = session.query(User).filter(User.id>2,User.name=='qqq222').first()
print(res)  # qqq222
res = session.query(User).filter_by(name='www222',email='111@qq.com').first()
print(res)  # www222

4.查询条件使用占位符:

from sqlalchemy.sql import text
res = session.query(User).filter(text("id>:value or name=:name")).params(value=7,name='henry').all()
print(res)  # [henry, qqq222, www222]

5.在sqlalchemy中用from_statement写原生sql,注意原生sql中表名需要写成models中用__tablename__定义的表名:

from sqlalchemy.sql import text
res = session.query(User).from_statement(text('select * from users where id=:id')).params(id=7).all()
print(res)  # [marry222]
"""
django执行原生sql:用raw(),在括号内编写sql语句:
res = models.User.objects.raw('select * from app01_user')
print(list(res))  # [<User: User object (1)>, <User: User object (2)>]
"""

6.筛选条件中加入范围:

res = session.query(User).filter(User.id.between(7,9)).all()
print(res)  # [marry222, qqq222, www222]

7.查询字段的值在指定列表中的数据:

# print(res)  # [marry222, qqq222, www222]
res = session.query(User).filter(User.id.in_([3,4,6])).all()
print(res)  # [henry, 小丽, 小红]

8.与或非:~代表非

res = session.query(User).filter(~User.id.in_([2,3,4,5,6])).all()
print(res)  # WHERE (users.id NOT IN (__[POSTCOMPILE_id_1]))  不加all()时打印原生sql
print(res)  #[max, marry222, qqq222, www222]

6.sqlalchemy写原生sql

方式一:由于多线程可能影响到数据安全,conn对象应该写在函数内部

from sqlalchemy import create_engine

engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8", )

conn = engine.raw_connection()
cursor=conn.cursor()
cursor.execute('select * from books')
print(cursor.fetchall())  # ((1, 'max'), (2, '红楼梦'), (3, '西游记'), (4, '西游记'), (5, '西游记'), (6, '水浒传'))

方式二:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from sqlalchemy.sql import text
from sqlalchemy.orm import scoped_session

engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8")

Session = sessionmaker(bind=engine)
session = scoped_session(Session)

# 查询
cursor = session.execute(text('select * from users'))
print(cursor.fetchall())

# 新增
cursor = session.execute(text('insert into books(name) values (:name)'),params={'name':'骆驼祥子'})
session.commit()
print(cursor.lastrowid)  # 7  新增这条数据的id

7.django写原生sql

利用fbv来举例:
views.py:

def index(request):
    students = Student.objects.raw('select * from app01_student where id=1')
	'''拿到的结果是rawqueryset,也可以通过索引取值'''
    print(students)  # <RawQuerySet: select * from app01_student where id=1>
    print(students[0].name)  # marry
    return HttpResponse('ddd')

urls.py:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index',views.index)
]

8.一对多关系创建

一对一:本身是一个表,拆成两个表,做一对一的关联,只不过关联字段唯一
一对多:关联字段写在多的一方
多对多:需要建立中间表,本质也是一对多

一对一表关系创建:

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, UniqueConstraint, Index
from sqlalchemy.orm import relationship
Base = declarative_base()

class Hobby(Base):
    __tablename__ = 'hobby'
    id = Column(Integer,primary_key=True)
    caption = Column(String(32),default='篮球')


class Person(Base):
    __tablename__ = 'person'
    id = Column(Integer,primary_key=True)
    name = Column(String(32),index=True,nullable=True)
    '''关联字段写在多的一方,跟hobby表中的id做关联'''
    hobby_id = Column(Integer,ForeignKey('hobby.id'))
    '''backref做反向查询'''
    hobby = relationship('Hobby',backref='pers')

    def __repr__(self):
        return self.name

engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user",)  # 需要手动创建一个名为name的库
Base.metadata.create_all(engine)

一对多新增:
方式一:首先需要在表模型中写入relationship字段,该字段和数据库无关,不会在表中显示该字段,作用是用来做反向查询,和django中的related_name类似。同时新增hobby数据和person数据:
models.py:

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, UniqueConstraint, Index
from sqlalchemy.orm import relationship
Base = declarative_base()

class Hobby(Base):
    __tablename__ = 'hobby'
    id = Column(Integer,primary_key=True)
    caption = Column(String(32),default='篮球')


class Person(Base):
    __tablename__ = 'person'
    id = Column(Integer,primary_key=True)
    name = Column(String(32),index=True,nullable=True)
    '''关联字段写在多的一方,跟hobby表中的id做关联'''
    hobby_id = Column(Integer,ForeignKey('hobby.id'))
    '''backref做反向查询,这个字段和数据库无关'''
    hobby = relationship('Hobby',backref='pers')

    def __repr__(self):
        return self.name

engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user",)  # 需要手动创建一个名为name的库
Base.metadata.create_all(engine)

flask.py:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import scoped_session
from models1 import Hobby, Person

engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8")
Session = sessionmaker(bind=engine)
session = scoped_session(Session)

Hobby(caption='soccer')
hobby_obj = Hobby(caption='soccer')
person=Person(name='小红',hobby=hobby_obj)
session.add_all([hobby_obj,person])
session.commit()
session.close()

image
方式二:如果外键字段已经存在于表中,首先查出来,用hobby_id新增:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import scoped_session
from models1 import Hobby, Person

engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user?charset=utf8")
Session = sessionmaker(bind=engine)
session = scoped_session(Session)

Hobby(caption='soccer')
hobby_obj = session.query(Hobby).filter(Hobby.caption=='soccer').first()
person=Person(name='小红',hobby_id=hobby_obj.id)
session.add_all([hobby_obj,person])
session.commit()
session.close()

9.一对多关系的查询(基于对象的跨表查)

flask中也有正反向查询。从外键字段开始查询为正向查询,对象点外键字段拿到是一个对象。反向查询点backref指定的值拿到该字段所在表的对象,类似于django中的related_name:

'''正向查询'''
person_obj = session.query(Person).filter(Person.id==6).first()
print(person_obj.hobby)  # soccer 因为Hobby表中重写了__str__,所以名字是caption的值,其实它是个对象
print(person_obj.hobby.id)  # 6

'''反向查询'''
hobby_obj = session.query(Hobby).filter(Hobby.id==6).first()
print(hobby_obj.pers)  # [小红, 小红]

10.多对多关系表建立

10.1 创建表

多对多关系需要手动创建中间表,relationship是为了方便方向查询

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, UniqueConstraint, Index
from sqlalchemy.orm import relationship

Base = declarative_base()


class Boy(Base):
    __tablename__ = 'boy'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), unique=True, nullable=False)
    '''这个字段不会在表中显示,只是为了方便查询,只需要写在一个表中即可,写在哪个表中从哪个表开始查,就是正向查询'''
    girls = relationship('Girl',secondary='boy2girl',backref='boys')

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name


class Girl(Base):
    __tablename__ = 'girl'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), unique=True, nullable=False)

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

class Boy2Girl(Base):
    __tablename__ = 'boy2girl'
    id = Column(Integer,primary_key=True,autoincrement=True)
    '''两张表分别建立两个外键'''
    girl_id = Column(Integer,ForeignKey('girl.id'))
    boy_id = Column(Integer,ForeignKey('boy.id'))

if __name__ == '__main__':
    engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user")
    Base.metadata.create_all(engine)

10.2 基于对象插入数据

'''正向插入'''
'''relationship字段在表Boy中,相当于是正向查询,直接点外键字段添加数据'''
boy_obj = Boy(name='彭于晏')
boy_obj.girls = [Girl(name='qqq'),Girl(name='www')]
session.add(boy_obj)
'''反向插入'''
girl_obj = Girl(name='迪丽热巴')
'''从Girl表到Boy表是反向查询,直接点Boy表中的relationship字段中的backref属性添加多对多数据'''
girl_obj.boys = [Boy(name='aaa'),Boy(name='sss')]
session.add(girl_obj)
session.commit()

11.多对多关系的查询(基于对象的跨表查)

'''基于对象的正向查询,直接点外键,得到的结果是一个列表'''
boy_obj = session.query(Boy).filter(Boy.id==2).first()
print(boy_obj.girls)  # [qqq, www]

'''基于对象的反向查询,直接点backref对应的值,得到的结果是一个列表'''
girl_obj = session.query(Girl).filter(Girl.id==4).first()
print(girl_obj.boys)  # [aaa, sss]

12.基于连表的跨表查询

django中不支持left join 和right join。没有外键关系也可以连表
models.py

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, UniqueConstraint, Index
from sqlalchemy.orm import relationship
Base = declarative_base()

class Hobby(Base):
    __tablename__ = 'hobby'
    id = Column(Integer,primary_key=True)
    caption = Column(String(32),default='篮球')

    def __str__(self):
        return self.caption

    def __repr__(self):
        return self.caption


class Person(Base):
    __tablename__ = 'person'
    id = Column(Integer,primary_key=True)
    name = Column(String(32),index=True,nullable=True)
    '''关联字段写在多的一方,跟hobby表中的id做关联'''
    hobby_id = Column(Integer,ForeignKey('hobby.id'))
    '''backref做反向查询,这个字段和数据库无关'''
    hobby = relationship('Hobby',backref='pers')

    def __str__(self):
        return self.caption

    def __repr__(self):
        return self.name

engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/user",)  # 需要手动创建一个名为name的库
Base.metadata.create_all(engine)

flask.py:

''''''
res = session.query(Person,Hobby).filter(Person.hobby_id==Hobby.id).all()
print(res)  # [(max, 篮球), (小红, soccer), (小红, soccer)]

'''连表查询内连接'''
res = session.query(Person.name,Hobby.caption).join(Hobby).all()
print(res)  # [('max', '篮球'), ('小红', 'soccer'), ('小红', 'soccer')]

'''连表查询左连接(flask中没有右连接,实现右连接只要将.join前后两个表换位置即可)'''
res = session.query(Person.name,Hobby.caption).join(Hobby,Person.hobby_id==Hobby.id,isouter=True).all()
print(res)  # [('max', '篮球'), ('小红', 'soccer'), ('小红', 'soccer')]
posted @   ERROR404Notfound  阅读(60)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
Title
点击右上角即可分享
微信分享提示