使用 SQLAlchemy ORM 構(gòu)建具有有意義關(guān)系的數(shù)據(jù)模型。創(chuàng)建一對(duì)一、一對(duì)多、多對(duì)一和多對(duì)多關(guān)系。
所有 Python 開(kāi)發(fā)人員都可以從 SQLAlchemy 中獲益。無(wú)論您是在尋找更好的方法來(lái)管理數(shù)據(jù)庫(kù)連接,還是為您的應(yīng)用程序構(gòu)建 ORM數(shù)據(jù)層,我們都沒(méi)有理由pip install sqlalchemy從 Python 詞匯中省略“”。
從事大型應(yīng)用程序的工程師絕大多數(shù)更喜歡通過(guò) ORM 處理數(shù)據(jù),而不是原始 SQL。對(duì)于那些有大量數(shù)據(jù)背景的人(比如我自己)來(lái)說(shuō),將 SQL 隱藏在 Python 對(duì)象后面的抽象可能會(huì)令人反感。為什么我們需要外鍵來(lái)執(zhí)行兩個(gè)表之間的 JOIN?為什么從事大型軟件工作的工程師似乎過(guò)度使用“一對(duì)多”與“多對(duì)多”關(guān)系等術(shù)語(yǔ),而 SQL 本身沒(méi)有這樣的術(shù)語(yǔ)?如果你也有這種感覺(jué),那么你就有很好的同伴了。
幾年后,我發(fā)現(xiàn) ORM確實(shí)減少了構(gòu)建應(yīng)用程序的工作量,而且不僅僅是“害怕 SQL”的人的拐杖。我們通過(guò)將敏感數(shù)據(jù)事務(wù)處理為可重現(xiàn)的代碼模式來(lái)節(jié)省大量時(shí)間,但與我們?cè)诎踩院屯暾苑矫娅@得的收益相比,這種好處相形見(jiàn)絀。ORM 不會(huì)編寫(xiě)破壞性的 SQL 查詢;人們確實(shí)如此。
所以,是的。令人惱火的是,編寫(xiě) ORM 的工程師與了解底層 SQL 的工程師使用不同的行話,并且設(shè)置 ORM 需要大量的前期工作,這令人惱火,但這是值得的。今天,我們通過(guò)學(xué)習(xí)如何在 SQLAlchemy 中定義表關(guān)系來(lái)解決 ORM 開(kāi)發(fā)中最困難的部分。
設(shè)置一些數(shù)據(jù)模型
我們已經(jīng)在上一篇文章中介紹了 SQLAlchemy 數(shù)據(jù)模型,因此我將跳過(guò)更詳細(xì)的細(xì)節(jié)。如果您通過(guò)瘋狂谷歌搜索有關(guān) SQLAlchemy 的問(wèn)題到達(dá)這里,您可能應(yīng)該了解什么是模型以及如何定義它們。
我們將創(chuàng)建一些模型來(lái)演示如何在它們之間創(chuàng)建 SQL 關(guān)系。本著博客的精神,我們將為User、Post和創(chuàng)建模型Comment:
"""聲明模型和關(guān)系。""" from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.sql import func from database import engine Base = declarative_base() class User(Base): """用戶帳號(hào)。""" __tablename__ = "user" id = Column(Integer, primary_key=True, autoincrement="auto") username = Column(String(255), unique=True, nullable=False) password = Column(Text, nullable=False) email = Column(String(255), unique=True, nullable=False) first_name = Column(String(255)) last_name = Column(String(255)) bio = Column(Text) avatar_url = Column(Text) role = Column(String(255)) last_seen = Column(DateTime) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, onupdate=func.now()) def __repr__(self): return f"<User {self.username}>" class Comment(Base): """用戶對(duì)博客文章生成的評(píng)論。""" __tablename__ = "comment" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer) post_id = Column(Integer, index=True) body = Column(Text) upvotes = Column(Integer, default=1) removed = Column(Boolean, default=False) created_at = Column(DateTime, server_default=func.now()) def __repr__(self): return f"<Comment {self.id}>" class Post(Base): """博客文章。""" __tablename__ = "post" id = Column(Integer, primary_key=True, index=True) author_id = Column(Integer) slug = Column(String(255), nullable=False, unique=True) title = Column(String(255), nullable=False) summary = Column(String(400)) feature_image = Column(String(300)) body = Column(Text) status = Column(String(255), nullable=False, default="unpublished") created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now()) def __repr__(self): return f"<Post {self.id}>" Base.metadata.create_all(engine)
一對(duì)多關(guān)系
一對(duì)多(或多對(duì)一)關(guān)系是最常見(jiàn)的數(shù)據(jù)庫(kù)關(guān)系類型。如何應(yīng)用這種關(guān)系的一個(gè)永恒的例子是客戶和訂單之間的業(yè)務(wù)關(guān)系。單個(gè)客戶有多個(gè)訂單,但訂單沒(méi)有多個(gè)客戶,因此該術(shù)語(yǔ)
使用我們的博客示例,讓我們看看對(duì)于擁有多個(gè)帖子或具有多個(gè)評(píng)論的帖子的作者來(lái)說(shuō),一對(duì)多關(guān)系可能是什么樣子:
... from sqlalchemy.orm import relationship class User(Base): """用戶帳號(hào)。""" __tablename__ = "user" id = Column(Integer, primary_key=True, autoincrement="auto") username = Column(String(255), unique=True, nullable=False) password = Column(Text, nullable=False) email = Column(String(255), unique=True, nullable=False) first_name = Column(String(255)) last_name = Column(String(255)) bio = Column(Text) avatar_url = Column(Text) role = Column(String(255)) last_seen = Column(DateTime) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, onupdate=func.now()) def __repr__(self): return f"<User {self.username}>" class Comment(Base): """用戶對(duì)博客文章生成的評(píng)論。""" __tablename__ = "comment" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("user.id")) # FK added post_id = Column(Integer, ForeignKey("post.id"), index=True) # FK added body = Column(Text) upvotes = Column(Integer, default=1) removed = Column(Boolean, default=False) created_at = Column(DateTime, server_default=func.now()) # Relationships user = relationship("User") def __repr__(self): return f"<Comment {self.id}>" class Post(Base): """博客文章/文章。""" __tablename__ = "post" id = Column(Integer, primary_key=True, index=True) author_id = Column(Integer, ForeignKey("user.id")) # FK added slug = Column(String(255), nullable=False, unique=True) title = Column(String(255), nullable=False) summary = Column(String(400)) feature_image = Column(String(300)) body = Column(Text) status = Column(String(255), nullable=False, default="unpublished") created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now()) # 人際關(guān)系 author = relationship("User") comments = relationship("Comment") def __repr__(self): return f"<Post {self.id}>"
我們?cè)谀P椭刑砑恿藘蓚€(gè)關(guān)鍵的附加功能,乍一看可能很難發(fā)現(xiàn)。首先,我們將一些屬性(列)設(shè)置為外鍵(如果您熟悉 SQL,那么應(yīng)該很好地轉(zhuǎn)到這里)。外鍵是列的屬性;當(dāng)存在外鍵時(shí),我們說(shuō)這個(gè)特定的列表示表之間的關(guān)系:一個(gè)表中最常見(jiàn)的項(xiàng)目“屬于”另一個(gè)表的項(xiàng)目,例如當(dāng)客戶“擁有”訂單時(shí),或者當(dāng)用戶“擁有”訂單時(shí)帖子。在我們的示例中,我們說(shuō)每個(gè)帖子都有一個(gè)由屬性指定的作者(用戶)author_id,如下所示:
定義兩個(gè)數(shù)據(jù)模型之間的外鍵關(guān)系:
... author_id = Column(Integer, ForeignKey("user.id")) ...
我們可以將用戶表和帖子表之間的數(shù)據(jù)結(jié)合起來(lái),這樣獲取一個(gè)表就可以讓我們獲得有關(guān)另一個(gè)表的信息。
這里的另一個(gè)新概念是關(guān)系。關(guān)系是對(duì)外鍵的補(bǔ)充,是告訴我們的應(yīng)用程序 (而不是數(shù)據(jù)庫(kù))我們正在兩個(gè)模型之間建立關(guān)系的一種方式。注意我們的外鍵的值是多少'user.id'。user 是我們表的表名User。將其與我們傳遞給關(guān)系的值進(jìn)行比較,該值是"User":目標(biāo)數(shù)據(jù)模型的類名(不是表名?。?。
定義兩個(gè)數(shù)據(jù)模型之間的關(guān)系:
... author = relationship("User") ...
外鍵告訴SQL我們正在構(gòu)建哪些關(guān)系,關(guān)系告訴我們的應(yīng)用程序我們正在構(gòu)建哪些關(guān)系。我們需要兩者兼而有之。
所有這一切的重點(diǎn)是能夠在我們的應(yīng)用程序中輕松執(zhí)行 JOIN。使用 ORM 時(shí),我們無(wú)法說(shuō)“將此模型與該模型連接起來(lái)”,因?yàn)槲覀兊膽?yīng)用程序不知道要連接哪些列。當(dāng)我們的模型中指定了關(guān)系時(shí),我們可以執(zhí)行諸如將兩個(gè)表連接在一起之類的操作,而無(wú)需指定任何進(jìn)一步的細(xì)節(jié):SQLAlchemy 將通過(guò)查看我們?cè)跀?shù)據(jù)模型中設(shè)置的內(nèi)容(由外鍵強(qiáng)制執(zhí)行)來(lái)知道如何連接表/模型和我們?cè)O(shè)定的關(guān)系)。我們實(shí)際上只是減輕了處理數(shù)據(jù)相關(guān)邏輯的負(fù)擔(dān),同時(shí)通過(guò)預(yù)先定義關(guān)系來(lái)創(chuàng)建應(yīng)用程序的業(yè)務(wù)邏輯。
如果表尚不存在,SQLAlchemy 僅從數(shù)據(jù)模型創(chuàng)建表。換句話說(shuō),如果我們第一次運(yùn)行應(yīng)用程序時(shí)存在錯(cuò)誤的關(guān)系,則第二次運(yùn)行應(yīng)用程序時(shí)錯(cuò)誤消息將持續(xù)存在,即使我們認(rèn)為已經(jīng)解決了問(wèn)題。要處理奇怪的錯(cuò)誤消息,每當(dāng)對(duì)模型進(jìn)行更改時(shí),請(qǐng)嘗試刪除 SQL 表,然后再次運(yùn)行應(yīng)用程序。
向后參考
指定數(shù)據(jù)模型上的關(guān)系允許我們通過(guò)原始模型上的屬性訪問(wèn)連接模型的屬性。如果我們要將Comment模型與User模型結(jié)合起來(lái),我們就可以通過(guò) 訪問(wèn)評(píng)論作者的屬性Comment.user.username,其中user是我們關(guān)系的名稱,username是關(guān)聯(lián)模型的屬性。
以這種方式創(chuàng)建的關(guān)系是單向的,因?yàn)槲覀兛梢酝ㄟ^(guò)球員訪問(wèn)球隊(duì)詳細(xì)信息,但無(wú)法從球隊(duì)訪問(wèn)球員詳細(xì)信息。我們可以通過(guò)設(shè)置反向引用輕松解決這個(gè)問(wèn)題。
創(chuàng)建關(guān)系時(shí),我們可以傳遞一個(gè)名為backref的屬性來(lái)使關(guān)系成為雙向的。以下是我們修改之前設(shè)置的關(guān)系的方法:
定義數(shù)據(jù)模型之間的雙向關(guān)系:
... # Relationships author = relationship("User", backref="posts") ...
有了backref,我們現(xiàn)在可以通過(guò)調(diào)用 來(lái)訪問(wèn)帖子的用戶詳細(xì)信息Post.author。
創(chuàng)建相關(guān)數(shù)據(jù)
是時(shí)候創(chuàng)建一些數(shù)據(jù)了。我們需要一個(gè)用戶作為我們博客的作者。當(dāng)我們這樣做時(shí),讓我們給他們一些博客文章:
from .models import Post, User admin_user = User( username="toddthebod", password="Password123lmao", email="todd@example.com", first_name="Todd", last_name="Birchard", bio="I write tutorials on the internet.", avatar_url="https://storage.googleapis.com/hackersandslackers-cdn/authors/todd_small@2x.jpg", role="admin", ) post_1 = Post( author_id=admin_user.id, slug="fake-post-slug", title="Fake Post Title", status="published", summary="A fake post to have some fake comments.", feature_image="https://cdn.hackersandslackers.com/2021/01/logo-smaller@2x.png", body="Cheese slices monterey jack cauliflower cheese dolcelatte cheese and wine fromage frais rubber cheese gouda. Rubber cheese cheese and wine cheeseburger cheesy grin paneer paneer taleggio caerphilly. Edam mozzarella.", ) post_2 = Post( author_id=admin_user.id, slug="an-additional-post", title="Yet Another Post Title", status="published", summary="An in-depth exploration into writing your second blog post.", feature_image="https://cdn.hackersandslackers.com/2021/01/logo-smaller@2x.png", body="Smelly cheese cheese slices fromage. Pepper jack taleggio monterey jack cheeseburger pepper jack swiss everyone loves. Cheeseburger say cheese brie fromage frais swiss when the cheese comes out everybody's happy babybel cheddar. Cheese and wine cheesy grin", )
我們創(chuàng)建了對(duì)象,但尚未將它們保存到數(shù)據(jù)庫(kù)中。我將為此組合幾個(gè)函數(shù);create_user()將處理用戶創(chuàng)建,并將create_post()創(chuàng)建......好吧,你知道:
"""通過(guò) SQLAlchemy 的 ORM 創(chuàng)建彼此相關(guān)的記錄。""" from typing import Tuple from sqlalchemy.exc import IntegrityError, SQLAlchemyError from sqlalchemy.orm import Session from logger import LOGGER from sqlalchemy_tutorial.part3_relationships.models import Post, User def create_user(session: Session, user: User) -> User: """ 如果用戶名尚未被占用,則創(chuàng)建一個(gè)新用戶. :param session: SQLAlchemy 數(shù)據(jù)庫(kù)Session。 :type session: Session :param user: 要?jiǎng)?chuàng)建的新用戶記錄。 :type user: User :return: Optional[User] """ try: existing_user = session.query(User).filter(User.username == user.username).first() if existing_user is None: session.add(user) # Add the user session.commit() # Commit the change LOGGER.success(f"創(chuàng)建的用戶: {user}") else: LOGGER.warning(f"用戶已存在于數(shù)據(jù)庫(kù)中:{existing_user}") return session.query(User).filter(User.username == user.username).first() except IntegrityError as e: LOGGER.error(e.orig) raise e.orig except SQLAlchemyError as e: LOGGER.error(f"創(chuàng)建用戶時(shí)出現(xiàn)意外錯(cuò)誤: {e}") raise e def create_post(session: Session, post: Post) -> Post: """ 創(chuàng)建帖子。 :param session: SQLAlchemy 數(shù)據(jù)庫(kù) session. :type session: Session :param post: 待創(chuàng)建的博客文章。 :type post: Post :return: Post """ try: existing_post = session.query(Post).filter(Post.slug == post.slug).first() if existing_post is None: session.add(post) # Add the post session.commit() # Commit the change LOGGER.success( f"已創(chuàng)建由用戶發(fā)布的帖子 {post} {post.author.username}" ) return session.query(Post).filter(Post.slug == post.slug).first() else: LOGGER.warning(f"數(shù)據(jù)庫(kù)中已存在帖子:{post}") return existing_post except IntegrityError as e: LOGGER.error(e.orig) raise e.orig except SQLAlchemyError as e: LOGGER.error(f"創(chuàng)建用戶時(shí)出現(xiàn)意外錯(cuò)誤:{e}") raise e
如果您對(duì)這些函數(shù)的復(fù)雜性感到措手不及,請(qǐng)知道除了session.add()和之外的所有內(nèi)容session.commit()都是為了錯(cuò)誤處理和避免重復(fù)而存在的。我們不想要重復(fù)的帖子或用戶,因此我們?cè)诶^續(xù)各自的功能之前檢查existing_user和。existing_post
讓我們創(chuàng)建這些記錄:
from .orm import create_user, create_post # 創(chuàng)建管理員用戶和兩個(gè)帖子 admin_user = create_user(session, admin_user) post_1 = create_post(session, post_1) post_2 = create_post(session, post_2)
我們現(xiàn)在有一個(gè)用戶和許多(ish)帖子!如果您要檢查數(shù)據(jù)庫(kù),您會(huì)看到在那里創(chuàng)建的這些記錄。該階段是為一對(duì)多連接查詢?cè)O(shè)置的。
執(zhí)行一對(duì)多 JOIN
當(dāng)我們?cè)?SQLAlchemy 模型上執(zhí)行 JOIN 時(shí),我們可以利用我們獲取的每個(gè)記錄的關(guān)系屬性,就好像該屬性本身就是一個(gè)完整的記錄一樣。向您展示我的意思可能會(huì)更容易。
在下面的查詢中,我們獲取數(shù)據(jù)庫(kù)中屬于用戶 1的所有帖子。然后,我們通過(guò)擴(kuò)展查詢來(lái)加入屬于該用戶的帖子.join(User, Post.author_id == User.id):
"""對(duì)具有關(guān)系的模型執(zhí)行 JOIN 查詢。""" from sqlalchemy.orm import Session from logger import LOGGER from sqlalchemy_tutorial.part3_relationships.models import Post, User def get_all_posts(session: Session, admin_user: User): """ 獲取屬于作者用戶的所有帖子。 :param session: SQLAlchemy database session. :type session: Session :param admin_user: 博客文章的作者。 :type admin_user: User :return: None """ posts = ( session.query(Post) .join(User, Post.author_id == User.id) .filter_by(username=admin_user.username) .all() ) for post in posts: post_record = { "post_id": post.id, "title": post.title, "summary": post.summary, "status": post.status, "feature_image": post.feature_image, "author": { "id": post.author_id, "username": post.author.username, "first_name": post.author.first_name, "last_name": post.author.last_name, "role": post.author.role, }, } LOGGER.info(post_record)
執(zhí)行查詢后,我們循環(huán)遍歷每條記錄并打印一個(gè) JSON 對(duì)象,該對(duì)象代表我們獲取的帖子及其作者數(shù)據(jù):
輸出get_all_posts():
{ "post_id": 1, "title": "Fake Post Title", "summary": "A fake post to have some fake comments.", "status": "published", "feature_image": "https://cdn.hackersandslackers.com/2021/01/logo-smaller@2x.png", "author": { "id": 2, "username": "toddthebod", "first_name": "Todd", "last_name": "Birchard", "role": "admin" } }, { "post_id": 2, "title": "Yet Another Post Title", "summary": "An in-depth exploration into writing your second blog post.", "status": "published", "feature_image": "https://cdn.hackersandslackers.com/2021/01/logo-smaller@2x.png", "author": { "id": 2, "username": "toddthebod", "first_name": "Todd", "last_name": "Birchard", "role": "admin" } }
多對(duì)多關(guān)系
當(dāng)我們期望關(guān)系中的一個(gè)表在另一個(gè)表中的多個(gè)記錄中只有一條記錄時(shí)(即:每個(gè)球隊(duì)一個(gè)球員),設(shè)置外鍵關(guān)系對(duì)我們很有幫助。如果球員可以屬于多個(gè)球隊(duì)怎么辦?這就是事情變得復(fù)雜的地方。
正如您可能已經(jīng)猜到的,多對(duì)多關(guān)系發(fā)生在表之間,其中表 1 中的 n 條記錄可以與表 2 中的 n 條記錄相關(guān)聯(lián)。SQLAlchemy 通過(guò)關(guān)聯(lián)表實(shí)現(xiàn)此類關(guān)系。關(guān)聯(lián)表是一張 SQL 表,其創(chuàng)建的唯一目的是解釋這些關(guān)系,我們將構(gòu)建一個(gè)關(guān)聯(lián)表。
看看我們?nèi)绾味x下面的association_table變量:
from sqlalchemy import Column, Integer, String, ForeignKey, Table from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() association_table = Table( 'association', Base.metadata, Column( 'team_id', Integer, ForeignKey('example.sqlalchemy_tutorial_players.team_id') ), Column( 'id', Integer, ForeignKey('example.sqlalchemy_tutorial_teams.id') ) ) class Player(Base): """Individual player belonging to a team.""" __tablename__ = "player" id = Column(Integer, primary_key=True, autoincrement="auto") team_id = Column(Integer, ForeignKey("team.id"), nullable=False) first_name = Column(String(255), nullable=False) last_name = Column(String(255), nullable=False) position = Column(String(100), nullable=False) injured = Column(Boolean) description = Column(Text, nullable=True) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, onupdate=func.now()) # Relationships team = relationship("Team") def __repr__(self): return f"<Player {self.id}>" class Team(Base): """Team consisting of many players.""" __tablename__ = "team" id = Column(Integer, primary_key=True, autoincrement="auto") name = Column(String(255), nullable=False) city = Column(String(255), nullable=False) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, onupdate=func.now()) def __repr__(self): return f"<Team {self.id}>"
我們使用新的數(shù)據(jù)類型Table來(lái)定義構(gòu)建多對(duì)多關(guān)聯(lián)的表。我們傳遞的第一個(gè)參數(shù)是結(jié)果表的名稱,我們將其命名為association。接下來(lái),我們將Base.metadata表與數(shù)據(jù)模型擴(kuò)展的相同聲明基礎(chǔ)關(guān)聯(lián)起來(lái)。最后,我們創(chuàng)建兩列作為我們關(guān)聯(lián)的每個(gè)表的外鍵:我們將Player的team_id列與Team的id列鏈接起來(lái)。
我們?cè)谶@里真正做的事情的本質(zhì)是創(chuàng)建第三個(gè)表來(lái)關(guān)聯(lián)我們的兩個(gè)表。我們還可以通過(guò)創(chuàng)建第三個(gè)數(shù)據(jù)模型來(lái)實(shí)現(xiàn)這一點(diǎn),但創(chuàng)建關(guān)聯(lián)表更簡(jiǎn)單一些。從現(xiàn)在開(kāi)始,我們現(xiàn)在可以直接查詢association_table以從我們的球員和球隊(duì)表中獲取記錄。
實(shí)現(xiàn)關(guān)聯(lián)表的最后一步是在我們的數(shù)據(jù)模型上設(shè)置關(guān)系。請(qǐng)注意我們?nèi)绾蜗裰耙粯釉赑layer上設(shè)置關(guān)系,但這次我們將次要屬性設(shè)置為等于關(guān)聯(lián)表的名稱。文章來(lái)源:http://www.zghlxwxcb.cn/article/578.html
[ { "comment_id": 1, "body_summary": "This post about SQLAlchemy is awful. You didnt ev...", "upvotes": 2, "comment_author_id": 3, "post": { "slug": "fake-post-slug", "title": "Fake Post Title", "post_author": "toddthebod" } }, { "comment_id": 2, "body_summary": "By the way, you SUCK!!! I HATE you!!!! I have a pr...", "upvotes": 5, "comment_author_id": 3, "post": { "slug": "fake-post-slug", "title": "Fake Post Title", "post_author": "toddthebod" } }, { "comment_id": 3, "body_summary": "YOU RUINED MY LIFE!!!!...", "upvotes": 5, "comment_author_id": 3, "post": { "slug": "fake-post-slug", "title": "Fake Post Title", "post_author": "toddthebod" } } ]
關(guān)鍵詞:SQLAlchemy 數(shù)據(jù)模型,關(guān)系,一對(duì)一,一對(duì)多,多對(duì)一,多對(duì)多,ORM,Python開(kāi)發(fā)文章來(lái)源地址http://www.zghlxwxcb.cn/article/578.html
到此這篇關(guān)于SQLAlchemy數(shù)據(jù)模型之間的關(guān)系 - 構(gòu)建有意義關(guān)系的數(shù)據(jù)模型的文章就介紹到這了,更多相關(guān)內(nèi)容可以在右上角搜索或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!