python之SQLAlchemy ORM
前言: 这篇博客主要介绍下SQLAlchemy及基本操作,写完后有空做个堡垒机小项目。有兴趣可看下python之数据库(mysql)操作。下篇博客整理写篇关于Web框架和django基础~~
一、ORM介绍
orm英文全称object relational mapping,就是对象映射关系程序,简单来说我们类似python这种面向对象的程序来说一切皆对象,但是我们使用的数据库却都是关系型的,为了保证一致的使用习惯,通过orm将编程语言的对象模型和数据库的关系模型建立映射关系,这样我们在使用编程语言对数据库进行操作的时候可以直接使用编程语言的对象模型进行操作就可以了,而不用直接使用sql语言。
orm的优点:
- 隐藏了数据访问细节,“封闭”的通用数据库交互,ORM的核心。他使得我们的通用数据库交互变得简单易行,并且完全不用考虑该死的SQL语句。快速开发,由此而来。
- ORM使我们构造固化数据结构变得简单易行。
缺点:
- 无可避免的,自动化意味着映射和关联管理,代价是牺牲性能(早期,这是所有不喜欢ORM人的共同点)。现在的各种ORM框架都在尝试使用各种方法来减轻这块(LazyLoad,Cache),效果还是很显著的。
二、SQLAlchemy框架与数据库API
在Python中,最有名的ORM框架是SQLAlchemy。用户包括openstack\Dropbox等知名公司或应用,主要用户列表http://www.sqlalchemy.org/organizations.html#openstack
需要自己把数据库中的表映射成类,然后才能通过对象的方式去调用。SQLAlchemy不止可以支持MYSQL,还可以支持Oracle等。
Dialect用于和数据API进行交流,根据配置文件的不同调用不同的数据库API,从而实现对数据库的操作:
MySQL-Python mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname> pymysql mysql+pymysql://<username>:<password>@<host>/<dbname>[?<options>] MySQL-Connector mysql+mysqlconnector://<user>:<password>@<host>[:<port>]/<dbname> cx_Oracle oracle+cx_oracle://user:pass@host:port/dbname[?key=value&key=value...] 更多详见:http://docs.sqlalchemy.org/en/latest/dialects/index.html
安装SQLAlchemy:
pip install SQLAlchemy
三、连接数据库并查询
1 from sqlalchemy import create_engine
2
3 #连接数据库,生成engine对象;最大连接数为5个
4 engine = create_engine("mysql+pymysql://root:root@127.0.0.1:3306/zcl", max_overflow=5)
5 print(engine) #Engine(mysql+pymysql://root:***@127.0.0.1:3306/zcl)
6 result = engine.execute('select * from students') #不用commit(),会自动commit
7 print(result.fetchall())
输出:
Engine(mysql+pymysql://root:***@127.0.0.1:3306/zcl) [(1, 'zcl', 'man', 22, '15622341234', None), (2, 'alex', 'man', 30, '15622341235', None), (5, 'Jack', 'man', 25, '1351234', 'CN'), (6, 'Mary', 'female', 18, '1341234', 'USA'), (10, 'Jack', 'man', 25, '1351234', 'CN'), (11, 'Jack2', 'man', 25, '1351234', 'CN'), (12, 'Mary', 'female', 18, '1341234', 'USA'), (13, 'cjy', 'man', 18, '1562234', 'USA'), (14, 'cjy2', 'man', 18, '1562235', 'USA'), (15, 'cjy3', 'man', 18, '1562235', 'USA'), (16, 'cjy4', 'man', 18, '1562235', 'USA'), (17, 'cjy5', 'man', 18, '1562235', 'USA')]
四、创建表
创建user与color表: 创建表时需要与MetaData的实例绑定。
1 from sqlalchemy import create_engine, \
2 Table, Column, Integer, String, MetaData, ForeignKey
3
4 metadata = MetaData() #相当于实例一个父类
5
6 user = Table('user', metadata, #相当于让Table继承metadata类
7 Column('id', Integer, primary_key=True),
8 Column('name', String(20)),
9 )
10
11 color = Table('color', metadata, #表名color
12 Column('id', Integer, primary_key=True),
13 Column('name', String(20)),
14 )
15 engine = create_engine("mysql+pymysql://root:root@localhost:3306/zcl", max_overflow=5)
16
17 metadata.create_all(engine) #table已经与metadate绑定
查看创建的表:
五、增删改查
1. 先来了解下原生sql语句的增删改查:
1 from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, ForeignKey,select
2
3 metadata = MetaData()
4
5 user = Table('user', metadata,
6 Column('id', Integer, primary_key=True),
7 Column('name', String(20)),
8 )
9
10 color = Table('color', metadata,
11 Column('id', Integer, primary_key=True),
12 Column('name', String(20)),
13 )
14 engine = create_engine("mysql+pymysql://root:root@127.0.0.1:3306/zcl", max_overflow=5)
15
16 conn = engine.connect() #创建游标,当前实例所处状态
17
18 # 创建SQL语句,INSERT INTO "user" (id, name) VALUES (:id, :name)
19 #id号可省略,默认是自增的
20 # conn.execute(user.insert(), {'id': 1, 'name': 'zcl'})
21 # conn.close()
22
23 # sql = user.insert().values(name='wu') #插入
24 # conn.execute(sql)
25 # conn.close()
26
27 #删除id号大于1的行,也可以where(user.c.name=="zcl")
28 # sql = user.delete().where(user.c.id > 1)
29 # conn.execute(sql)
30 # conn.close()
31
32 # 将name=="wuu"更改为"name=="ed"
33 # sql = user.update().where(user.c.name == 'wuu').values(name='ed')
34 # conn.execute(sql)
35 # conn.close()
36
37 #查询 下面不能写 sql = user.select... 会曝错
38 #sql = select([user, ]) #[(1, 'zcl'), (9, 'ed'), (10, 'ed')]
39 # sql = select([user.c.id, ]) #[(1,), (9,), (10,)]
40 sql = select([user.c.name, color.c.name]).where(user.c.id==color.c.id)
41 # sql = select([user.c.name]).order_by(user.c.name)
42 # sql = user.select([user]).group_by(user.c.name)
43
44 result = conn.execute(sql)
45 print(result.fetchall())
46 conn.close()
2. 通过SQLAlchemy的增删改查(重要):
1 from sqlalchemy import create_engine
2 from sqlalchemy.ext.declarative import declarative_base
3 from sqlalchemy import Column,Integer,String
4 from sqlalchemy.orm import sessionmaker
5
6 Base = declarative_base() #生成一个SqlORM基类(已经封装metadata)
7 #echo=True可以查看创建表的过程
8 engine = create_engine("mysql+pymysql://root:root@localhost:3306/zcl", echo=True)
9
10 class Host(Base):
11 __tablename__ = 'hosts' #表名为host
12 id = Column(Integer, primary_key=True, autoincrement=True)
13 hostname = Column(String(64), unique=True, nullable=False)
14 ip_addr = Column(String(128), unique=True, nullable=False)
15 port = Column(Integer, default=22)
16
17
18 Base.metadata.create_all(engine) #创建所有表结构
19
20 if __name__ == '__main__':
21 #创建与数据库的会话sessionclass,注意,这里返回给session的是个class类,不是实例
22 SessionCls=sessionmaker(bind=engine)
23 session=SessionCls() #连接的实例
24 #准备插入数据
25 h1 = Host(hostname='localhost', ip_addr='127.0.0.1') #实例化(未创建)
26 h2 = Host(hostname='ubuntu', ip_addr='192.168.2.243', port=20000)
27
28 #session.add(h1) #也可以用下面的批量处理
29 #session.add_all([h1,h2])
30 #h2.hostname='ubuntu_test' #只要没提交,此时修改也没问题
31
32 #查询数据,返回一个对象
33 obj = session.query(Host).filter(Host.hostname=="localhost").first()
34 print("-->",obj)
35 #[<__main__.Hostobjectat0x00000000048DC0B8>]如果上面为.all()
36 #<__main__.Hostobjectat0x000000000493C208>如果上面为.first()
37
38 #如果用.all(),会曝错AttributeError:'list'objecthasnoattribute'hostname'
39 #obj.hostname = "localhost_1" #将主机名修改为localhost_1
40
41 session.delete(obj) #删除行
42
43 session.commit()#提交
操作结果截图:
六、外键关联
1. 创建主机表hosts与分组表group,并建立关联,即一个组可对应多个主机:
1 from sqlalchemy import create_engine
2 from sqlalchemy.ext.declarative import declarative_base
3 from sqlalchemy import Column, Integer, String,ForeignKey
4 from sqlalchemy.orm import sessionmaker,relationship
5
6 Base = declarative_base() # 生成一个SqlORM 基类(已经封闭metadata)
7 #echo=True可以查看创建表的过程
8 engine = create_engine("mysql+pymysql://root:root@localhost:3306/zcl", echo=True)
9
10 class Host(Base):
11 __tablename__ = 'hosts' #表名
12 id = Column(Integer, primary_key=True, autoincrement=True) #默认自增
13 hostname = Column(String(64), unique=True, nullable=False)
14 ip_addr = Column(String(128), unique=True, nullable=False)
15 port = Column(Integer, default=22)
16 #外键关联,主机与组名关联,一个组对应多个主机
17 group_id = Column(Integer, ForeignKey("group.id"))
18
19
20 class Group(Base):
21 __tablename__ = "group"
22 id = Column(Integer,primary_key=True)
23 name = Column(String(64), unique=True, nullable=False)
24
25
26 Base.metadata.create_all(engine) # 创建所有表结构
27
28 if __name__ == '__main__':
29 # 创建与数据库的会话session class ,注意,这里返回给session的是个class,不是实例
30 SessionCls = sessionmaker(bind=engine)
31 session = SessionCls() #连接的实例
32
33 session.commit() #提交
查看结果:
问题: 查看新建的group表结构或从group表查询会发现desc group;select * from group都会曝错!!(为什么会产生这种错误可能是group与数据库有某些关联导致的,eg:group by... 我猜的)
解决方法: 用desc zcl.group; select * from zcl.group; (zcl为数据库名)
2. 创建完表后就要在表中创建数据啦。接下来在hosts表与group表创建数据:
1 from sqlalchemy import create_engine 2 from sqlalchemy.ext.declarative import declarative_base 3 from sqlalchemy import Column, Integer, String,ForeignKey 4 from sqlalchemy.orm import sessionmaker 5 6 Base = declarative_base() # 生成一个SqlORM 基类(已经封闭metadata) 7 #echo=True可以查看创建表的过程 8 engine = create_engine("mysql+pymysql://root:root@localhost:3306/zcl", echo=True) 9 10 class Host(Base): 11 __tablename__ = 'hosts' #表名 12 id = Column(Integer, primary_key=True, autoincrement=True) #默认自增 13 hostname = Column(String(64), unique=True, nullable=False) 14 ip_addr = Column(String(128), unique=True, nullable=False) 15 port = Column(Integer, default=22) 16 #外键关联,主机与组名关联 17 group_id = Column(Integer, ForeignKey("group.id")) 18 19 20 class Group(Base): 21 __tablename__ = "group" 22 id = Column(Integer,primary_key=True) 23 name = Column(String(64), unique=True, nullable=False) 24 25 Base.metadata.create_all(engine) # 创建所有表结构 26 27 if __name__ == '__main__': 28 # 创建与数据库的会话session class ,注意,这里返回给session的是个class,不是实例 29 SessionCls = sessionmaker(bind=engine) 30 session = SessionCls() #连接的实例 31 32 g1 = Group(name = "g1") 33 g2 = Group(name = "g2") 34 g3 = Group(name = "g3") 35 g4 = Group(name = "g4") 36 session.add_all([g1,g2,g3,g4]) 37 38 #此时上面的g1,g2,g3三条记录还未存在,因为程序运行到这一行时还未commit(),故g1.id也未存在,但是下面一行代码是用到g1.id的!!经过测试: 运行时虽然不曝错,但关联不成功,如下图 39 h1 = Host(hostname='localhost', ip_addr='127.0.0.1',group_id=g1.id) 40 session.add(h1) 41 42 session.commit() #提交
经过测试: 运行时虽然不曝错,但关联不成功,如下图:
3. 现在问题又来了,hosts表中的group_id可是为空啊!! 这肯定不行的。现在如何在不删除hosts表数据的前提下,使group_id不为空(eg: 使group_id为4,与g4建立关联)??可用下面的代码:
g4 = session.query(Group).filter(Group.name=="g4").first() #找到g4组的对象 h = session.query(Host).filter(Host.hostname=="localhost").update({"group_id":g4.id}) #更新(修改) session.commit() #提交
4. 问题: 如何获取与主机关联的group_id??
g4=session.query(Group).filter(Group.name=="g4").first() h=session.query(Host).filter(Host.hostname=="localhost").first() print("h1:",h.group_id)
好吧,我承认这个问题太简单了,通过上面的代码,找到主机的对象h, 则h.group_id就是答案。接下来的问题才是重点。
5. 此时可以获取已经关联的group_id,但如何获取已关联的组的组名??
print(h.group.name) #AttributeError:'Host'object has no attribute 'group'
嗯,你是初学者,你当然会说通过过h.group.name就可以找到与主机关联的组名! BUT,这是不行的,会曝错,因为Host类根本就没有group属性!!
解决方法:
- first:
from sqlalchemy.orm import relationship #导入relationship
- second:
在Host类中加入group = relationship("Group"):
class Host(Base):
__tablename__ = 'hosts' #表名
id = Column(Integer, primary_key=True, autoincrement=True) #默认自增
hostname = Column(String(64), unique=True, nullable=False)
ip_addr = Column(String(128), unique=True, nullable=False)
port = Column(Integer, default=22)
#外键关联,主机与组名关联
group_id = Column(Integer, ForeignKey("group.id"))
group = relationship("Group")
此时再用print(h.group.name)就不会曝错啦!!
6. 哈哈,问题还没完呢。 前面已经实现:通过主机可查看对应组名,那么如何实现通过组名查看对应的主机??
经过前面5个点的历练,你已成为小小的老司机了,于是你很自信地说: 和第5个点一样,在Group类中加入hosts = relationship("Host");
1 class Host(Base): 2 __tablename__ = 'hosts' #表名 3 id = Column(Integer,primary_key=True, autoincrement=True) #默认自增 4 hostname = Column(String(64), unique=True, nullable=False) 5 ip_addr = Column(String(128), unique=True, nullable=False) 6 port = Column(Integer, default=22) 7 #外键关联,主机与组名关联 8 group_id = Column(Integer,ForeignKey("group.id")) 9 group = relationship("Group") 10 11 class Group(Base): 12 __tablename__ = "group" 13 id = Column(Integer, primary_key=True) 14 name = Column(String(64), unique=True, nullable=False) 15 hosts = relationship("Host") 16 17 Base.metadata.create_all(engine) #创建所有表结构 18 19 g4 = session.query(Group).filter(Group.name=="g4").first() 20 h = session.query(Host).filter(Host.hostname=="localhost").first() 21 print("h1:",h.group_id) #h1: 4 22 #此时可以获取已经关联的group_id,但如何获取已关联的组的组名 23 print(h.group.name) #g4 24 print("g4:",g4.hosts) #g4:[<__main__.Hostobjectat0x0000000004303860>]
7. 通过上面的两句代码可实现双向关联。但必须在两个表都加上一句代码才行,有没有办法只用一句代码就实现双向关联?? 当然有,老司机会这么做:
在Host类中加入下面这句代码,即可实现双向关联:
group=relationship("Group",backref="host_list")
八、合并查询join
合并查询分为: inner join、left outer join、right outer join、full outer join
下面的例子可以让你完全理解join: http://stackoverflow.com/questions/38549/what-is-the-difference-between-inner-join-and-outer-join
关于join的原生sql操作:
在SQLAlchemy实现sql.join:
obj = session.query(Host).join(Host.group).all() #相当于inner join print("-->obj:",obj)
九、分类聚合group by
group by是啥意思呢? 我说下我的理解吧,group即分组,by为通过;合起来即: 通过XX分组;
举个例子吧,现在有两张表,分别是主机表与分组表。两表已经通过group_id建立关联,分组表中有4个数据,分别为g1,g2,g3,g4; id分别为1,2,3,4; 而主机表有3个数据,group_id分别为4,3,4; id分别为1,2,4; 现在对hosts表执行group by命令,进行分类聚合。
具体请看下图:
对应SQLAlchemy语句:
obj1 = session.query(Host).join(Host.group).group_by(Group.name).all() #分类聚合 print("-->obj1:",obj1)
对应SQLAlchemy语句:
obj2 = session.query(Host,func.count(Group.name)).join(Host.group).group_by(Group.name).all() print("-->obj2:",obj2) 输出: -->obj2: [(<__main__.Host object at 0x0000000003C854A8>, 1), (<__main__.Host object at 0x0000000003C85518>, 2)]
十、多对多关联
多对多关联,即: 一个主机h1可对应在多个组(g1,g2),一个组(g1)可对应多个主机(h1,h2)
想实现如下的多对多关联,需要一张中间表。Eg:
h1 g1
h1 g2
h2 g1
Host表
h1
h2
h3
Group表
g1
g2
g3
HostToGroup中间表(实现多对多关联,sqlalchemy也是这样实现的)
id host_id group_id
1 1 1
2 1 2
3 2 1
虽然有了中间表,但如果想查看一个组对应的所有主机名或者一个主机对应的所有组,还是需要Group/Host与中间表进行一系列的关联操作(join~), 但SqlAlchemy简化了关联操作!!
调用下面命令便会自动关联中间表:
Host.groups() #查看一个主机对应所有组 Group.hosts()
SQLAlchemy是如何实现多对多关联的??
1. 建立中间表,关联其它两个表
1 from sqlalchemy import create_engine,func,Table
2 from sqlalchemy.ext.declarative import declarative_base
3 from sqlalchemy import Column, Integer, String,ForeignKey
4 from sqlalchemy.orm import sessionmaker,relationship
5
6 Base = declarative_base() # 生成一个SqlORM 基类(已经封闭metadata)
7 #echo=True可以查看创建表的过程
8 engine = create_engine("mysql+pymysql://root:root@localhost:3306/zcl", echo=True)
9
10
11 #直接创建表并返回表的实例 Host2Group主动关联Host与Group(被关联)
12 Host2Group = Table('host_to_group',Base.metadata,
13 Column('host_id',ForeignKey('host.id'),primary_key=True),
14 Column('group_id',ForeignKey('group.id'),primary_key=True),
15 #一个表为什么能创建两个主键(其实是两个列同时作为主键,非空且唯一)
16 #PRIMARY KEY (host_id, group_id),
17 )
2. 在Host表(或Group表)指定中间表的实例,加上backref就不用在Group表中指定
1 #声明表的映射关系
2 class Host(Base):
3 __tablename__ = 'host' #表名
4 id = Column(Integer, primary_key=True, autoincrement=True) #默认自增
5 hostname = Column(String(64), unique=True, nullable=False)
6 ip_addr = Column(String(128), unique=True, nullable=False)
7 port = Column(Integer, default=22)
8 #外键关联,主机与组名关联
9 #group_id = Column(Integer, ForeignKey("group.id"))
10 groups = relationship("Group", #关联Group表
11 secondary = Host2Group, #关联第三方表
12 backref = "host_list") #双向关联,不用在Group类中再加这句代码
13
14 def __repr__(self):
15 return "<id=%s,hostname=%s,ip_addr=%s>" % (self.id,
16 self.hostname,
17 self.ip_addr)
3. 创建组与主机
1 if __name__ == '__main__':
2 SessionCls = sessionmaker(bind=engine)
3 session = SessionCls()
4 """
5 g1 = Group(name = "g1")
6 g2 = Group(name = "g2")
7 g3 = Group(name = "g3")
8 g4 = Group(name = "g4")
9 session.add_all([g1,g2,g3,g4])
10 """
11 """
12 h1 = Host(hostname="h1",ip_addr="10.1.1.1")
13 h2 = Host(hostname="h2",ip_addr="10.1.1.2",port=10000)
14 h3 = Host(hostname="h3",ip_addr="10.1.1.3",port=6666)
15 session.add_all([h1,h2,h3])
16 """
4. 建立关联与查询
1 """ 2 groups = session.query(Group).all() 3 h1 = session.query(Host).filter(Host.hostname=="h1").first() 4 h1.groups = groups #将h1关联到所有的组 5 print("-->:",h1.groups) 6 h1.groups.pop() #删除一个关联 7 """ 8 h2 = session.query(Host).filter(Host.hostname=="h2").first() 9 #h2.groups = groups[1:-1] #将h2关联到组(2和3) 10 print("=======>h2.groups:",h2.groups) 11 #=======>h2.groups: [<__main__.Group object at 0x00000000044A3F98>, 12 # <__main__.Group object at 0x00000000044A3FD0>] 13 #加上__repr__()后,变为=======>h2.groups: [<id=2,name=g2>, <id=3,name=g3>] 14 15 g1 = session.query(Group).first() 16 print("=======>g1:",g1.host_list) 17 #=======>g1: [<id=1,hostname=h1,ip_addr=10.1.1.1>] 18 session.commit()
测试截图:
查看表结构:
查看表内容:
查看第三方表:
完整例子:
1 from sqlalchemy import create_engine,func,Table 2 from sqlalchemy.ext.declarative import declarative_base 3 from sqlalchemy import Column, Integer, String,ForeignKey 4 from sqlalchemy.orm import sessionmaker,relationship 5 6 Base = declarative_base() # 生成一个SqlORM 基类(已经封闭metadata) 7 #echo=True可以查看创建表的过程 8 engine = create_engine("mysql+pymysql://root:root@localhost:3306/zcl", echo=True) 9 10 11 #直接创建表并返回表的实例 Host2Group主动关联Host与Group(被关联) 12 Host2Group = Table('host_to_group',Base.metadata, 13 Column('host_id',ForeignKey('host.id'),primary_key=True), 14 Column('group_id',ForeignKey('group.id'),primary_key=True), 15 #一个表为什么能创建两个主键(其实是两个列同时作为主键,非空且唯一) 16 #PRIMARY KEY (host_id, group_id), 17 ) 18 19 20 #声明表的映射关系 21 class Host(Base): 22 __tablename__ = 'host' #表名 23 id = Column(Integer, primary_key=True, autoincrement=True) #默认自增 24 hostname = Column(String(64), unique=True, nullable=False) 25 ip_addr = Column(String(128), unique=True, nullable=False) 26 port = Column(Integer, default=22) 27 #外键关联,主机与组名关联 28 #group_id = Column(Integer, ForeignKey("group.id")) 29 groups = relationship("Group", #关联Group表 30 secondary = Host2Group, #关联第三方表 31 backref = "host_list")#双向关联,不用在Group类中再加这句代码 32 33 def __repr__(self): 34 return "<id=%s,hostname=%s,ip_addr=%s>" % (self.id, 35 self.hostname, 36 self.ip_addr) 37 38 class Group(Base): 39 __tablename__ = "group" 40 id = Column(Integer,primary_key=True) 41 name = Column(String(64), unique=True, nullable=False) 42 43 def __repr__(self): 44 return "<id=%s,name=%s>" % (self.id, self.name) 45 46 47 Base.metadata.create_all(engine) # 创建所有表结构 48 49 if __name__ == '__main__': 50 SessionCls = sessionmaker(bind=engine) 51 session = SessionCls() 52 """ 53 g1 = Group(name = "g1") 54 g2 = Group(name = "g2") 55 g3 = Group(name = "g3") 56 g4 = Group(name = "g4") 57 session.add_all([g1,g2,g3,g4]) 58 """ 59 """ 60 h1 = Host(hostname="h1",ip_addr="10.1.1.1") 61 h2 = Host(hostname="h2",ip_addr="10.1.1.2",port=10000) 62 h3 = Host(hostname="h3",ip_addr="10.1.1.3",port=6666) 63 session.add_all([h1,h2,h3]) 64 """ 65 """ 66 groups = session.query(Group).all() 67 h1 = session.query(Host).filter(Host.hostname=="h1").first() 68 h1.groups = groups #将h1关联到所有的组 69 print("-->:",h1.groups) 70 h1.groups.pop() #删除一个关联 71 """ 72 h2 = session.query(Host).filter(Host.hostname=="h2").first() 73 #h2.groups = groups[1:-1] 74 print("=======>h2.groups:",h2.groups) 75 #=======>h2.groups: [<__main__.Group object at 0x00000000044A3F98>, 76 # <__main__.Group object at 0x00000000044A3FD0>] 77 #加上__repr__()后,变为=======>h2.groups: [<id=2,name=g2>, <id=3,name=g3>] 78 79 g1 = session.query(Group).first() 80 print("=======>g1:",g1.host_list) 81 #=======>g1: [<id=1,hostname=h1,ip_addr=10.1.1.1>] 82 session.commit()
出处:http://www.cnblogs.com/0zcl
文章未标明转载则为原创博客。欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
1.非系统的学习也是在浪费时间
2.做一个会欣赏美,懂艺术,会艺术的技术人