跳到主要內容

MongoDB via Dotnet Core數據映射詳解


用好數據映射,MongoDB via Dotnet Core開發變會成一件超級快樂的事。




一、前言


MongoDB這幾年已經成為NoSQL的頭部數據庫。


由於MongoDB free schema的特性,使得它在互聯網應用方面優於常規數據庫,成為了相當一部分大廠的主數據選擇;而它的快速布署和開發簡單的特點,也吸引着大量小開發團隊的支持。


關於MongoDB快速布署,我在15分鐘從零開始搭建支持10w+用戶的生產環境(二)里有寫,需要了可以去看看。



作為一個數據庫,基本的操作就是CRUD。MongoDB的CRUD,不使用SQL來寫,而是提供了更簡單的方式。


方式一、BsonDocument方式


BsonDocument方式,適合能熟練使用MongoDB Shell的開發者。MongoDB Driver提供了完全覆蓋Shell命令的各種方式,來處理用戶的CRUD操作。


這種方法自由度很高,可以在不需要知道完整數據集結構的情況下,完成數據庫的CRUD操作。


方式二、數據映射方式


數據映射是最常用的一種方式。準備好需要處理的數據類,直接把數據類映射到MongoDB,並對數據集進行CRUD操作。



下面,對數據映射的各個部分,我會逐個說明。


    為了防止不提供原網址的轉載,特在這裏加上原文鏈接:https://www.cnblogs.com/tiger-wang/p/13185605.html


二、開發環境&基礎工程


這個Demo的開發環境是:Mac + VS Code + Dotnet Core 3.1.2。


建立工程:


% dotnet new sln -o demo
The template "Solution File" was created successfully.
cd demo 
% dotnet new console -o demo
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on demo/demo.csproj...
  Determining projects to restore...
  Restored demo/demo/demo.csproj (in 162 ms).

Restore succeeded.
% dotnet sln add demo/demo.csproj 
Project `demo/demo.csproj` added to the solution.

建立工程完成。


下面,增加包mongodb.driver到工程:


cd demo
% dotnet add package mongodb.driver
  Determining projects to restore...
info : Adding PackageReference for package 'mongodb.driver' into project 'demo/demo/demo.csproj'.
info : Committing restore...
info : Writing assets file to disk. Path: demo/demo/obj/project.assets.json
log  : Restored /demo/demo/demo.csproj (in 6.01 sec).

項目準備完成。


看一下目錄結構:


% tree .
.
├── demo
│   ├── Program.cs
│   ├── demo.csproj
│   └── obj
│       ├── demo.csproj.nuget.dgspec.json
│       ├── demo.csproj.nuget.g.props
│       ├── demo.csproj.nuget.g.targets
│       ├── project.assets.json
│       └── project.nuget.cache
└── demo.sln


mongodb.driver是MongoDB官方的數據庫SDK,從Nuget上安裝即可。


三、Demo準備工作


創建數據映射的模型類CollectionModel.cs,現在是個空類,後面所有的數據映射相關內容會在這個類進行說明:


public class CollectionModel
{

}

並修改Program.cs,準備Demo方法,以及連接數據庫:


class Program
{

    private const string MongoDBConnection = "mongodb://localhost:27031/admin";

    private static IMongoClient _client = new MongoClient(MongoDBConnection);
    private static IMongoDatabase _database = _client.GetDatabase("Test");
    private static IMongoCollection<CollectionModel> _collection = _database.GetCollection<CollectionModel>("TestCollection");

    static async Task Main(string[] args)
    
{
        await Demo();
        Console.ReadKey();
    }

    private static async Task Demo()
    
{
    }
}

四、字段映射


從上面的代碼中,我們看到,在生成Collection對象時,用到了CollectionModel


IMongoDatabase _database = _client.GetDatabase("Test");
IMongoCollection<CollectionModel> _collection = _database.GetCollection<CollectionModel>("TestCollection");

這兩行,其實就完成了一個映射的工作:把MongoDB中,Test數據庫下,TestCollection數據集(就是SQL中的數據表),映射到CollectionModel這個數據類中。換句話說,就是用CollectionModel這個類,來完成對數據集TestCollection的所有操作。



保持CollectionModel為空,我們往數據庫寫入一行數據:


private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel();
    await _collection.InsertOneAsync(new_item);
}

執行,看一下寫入的數據:



    "_id" : ObjectId("5ef1d8325327fd4340425ac9")
}

OK,我們已經寫進去一條數據了。因為映射類是空的,所以寫入的數據,也只有_id一行內容。



但是,為什麼會有一個_id呢?


1. ID字段


MongoDB數據集中存放的數據,稱之為文檔(Document)。每個文檔在存放時,都需要有一個ID,而這個ID的名稱,固定叫_id


當我們建立映射時,如果給出_id字段,則MongoDB會採用這個ID做為這個文檔的ID,如果不給出,MongoDB會自動添加一個_id字段。


例如:


public class CollectionModel
{

    public ObjectId _id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
}


public class CollectionModel
{

    public string title { get; set; }
    public string content { get; set; }
}

在使用上是完全一樣的。唯一的區別是,如果映射類中不寫_id,則MongoDB自動添加_id時,會用ObjectId作為這個字段的數據類型。


ObjectId是一個全局唯一的數據。


當然,MongoDB允許使用其它類型的數據作為ID,例如:stringintlongGUID等,但這就需要你自己去保證這些數據不超限並且唯一。


例如,我們可以寫成:


public class CollectionModel
{

    public long _id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
}


我們也可以在類中修改_id名稱為別的內容,但需要加一個描述屬性BsonId


public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
}

這兒特別要注意:BsonId屬性會告訴映射,topic_id就是這個文檔數據的ID。MongoDB在保存時,會將這個topic_id轉成_id保存到數據集中。


在MongoDB數據集中,ID字段的名稱固定叫_id。為了代碼的閱讀方便,可以在類中改為別的名稱,但這不會影響MongoDB中存放的ID名稱。



修改Demo代碼:


private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel()
    {
        title = "Demo",
        content = "Demo content",
    };
    await _collection.InsertOneAsync(new_item);
}

跑一下Demo,看看保存的結果:



    "_id" : ObjectId("5ef1e1b1bc1e18086afe3183"), 
    "title" : "Demo"
    "content" : "Demo content"
}

2. 簡單字段


就是常規的數據字段,直接寫就成。


public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
}

保存后的數據:



    "_id" : ObjectId("5ef1e9caa9d16208de2962bb"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100)
}

3. 一個的特殊的類型 - Decimal


說Decimal特殊,是因為MongoDB在早期,是不支持Decimal的。直到MongoDB v3.4開始,數據庫才正式支持Decimal。


所以,如果使用的是v3.4以後的版本,可以直接使用,而如果是以前的版本,需要用以下的方式:


[BsonRepresentation(BsonType.Double, AllowTruncation = true)]
public decimal price { get; set; }

其實就是把Decimal通過映射,轉為Double存儲。


4. 類字段


把類作為一個數據集的一個字段。這是MongoDB作為文檔NoSQL數據庫的特色。這樣可以很方便的把相關的數據組織到一條記錄中,方便展示時的查詢。


我們在項目中添加兩個類ContactAuthor


public class Contact
{

    public string mobile { get; set; }
}

public class Author
{

    public string name { get; set; }
    public List<Contact> contacts { get; set; }
}

然後,把Author加到CollectionModel中:


public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
    public Author author { get; set; }
}

嗯,開始變得有點複雜了。


完善Demo代碼:


private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel()
    {
        title = "Demo",
        content = "Demo content",
        favor = 100,
        author = new Author
        {
            name = "WangPlus",
            contacts = new List<Contact>(),
        }
    };

    Contact contact_item1 = new Contact()
    {
        mobile = "13800000000",
    };
    Contact contact_item2 = new Contact()
    {
        mobile = "13811111111",
    };
    new_item.author.contacts.Add(contact_item1);
    new_item.author.contacts.Add(contact_item2);

    await _collection.InsertOneAsync(new_item);
}

保存的數據是這樣的:



    "_id" : ObjectId("5ef1e635ce129908a22dfb5e"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100),
    "author" : {
        "name" : "WangPlus"
        "contacts" : [
            {
                "mobile" : "13800000000"
            }, 
            {
                "mobile" : "13811111111"
            }
        ]
    }
}

這樣的數據結構,用着不要太爽!


5. 枚舉字段


枚舉字段在使用時,跟類字段相似。


創建一個枚舉TagEnumeration


public enum TagEnumeration
{
    CSharp = 1,
    Python = 2,
}

加到CollectionModel中:


public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
    public Author author { get; set; }
    public TagEnumeration tag { get; set; }
}

修改Demo代碼:


private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel()
    {
        title = "Demo",
        content = "Demo content",
        favor = 100,
        author = new Author
        {
            name = "WangPlus",
            contacts = new List<Contact>(),
        },
        tag = TagEnumeration.CSharp,
    };
    /* 後邊代碼略過 */
}

運行后看數據:



    "_id" : ObjectId("5ef1eb87cbb6b109031fcc31"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100), 
    "author" : {
        "name" : "WangPlus"
        "contacts" : [
            {
                "mobile" : "13800000000"
            }, 
            {
                "mobile" : "13811111111"
            }
        ]
    }, 
    "tag" : NumberInt(1)
}

在這裏,tag保存了枚舉的值。


我們也可以保存枚舉的字符串。只要在CollectionModel中,tag聲明上加個屬性:


public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
    public Author author { get; set; }
    [BsonRepresentation(BsonType.String)]
    public TagEnumeration tag { get; set; }
}

數據會變成:



    "_id" : ObjectId("5ef1ec448f1d540919d15904"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100), 
    "author" : {
        "name" : "WangPlus"
        "contacts" : [
            {
                "mobile" : "13800000000"
            }, 
            {
                "mobile" : "13811111111"
            }
        ]
    }, 
    "tag" : "CSharp"
}

6. 日期字段


日期字段會稍微有點坑。


這個坑其實並不源於MongoDB,而是源於C#的DateTime類。我們知道,時間根據時區不同,時間也不同。而DateTime並不準確描述時區的時間。


我們先在CollectionModel中增加一個時間字段:


public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
    public Author author { get; set; }
    [BsonRepresentation(BsonType.String)]
    public TagEnumeration tag { get; set; }
    public DateTime post_time { get; set; }
}

修改Demo:


private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel()
    {
        /* 前邊代碼略過 */
        post_time = DateTime.Now, /* 2020-06-23T20:12:40.463+0000 */
    };
    /* 後邊代碼略過 */
}

運行看數據:



    "_id" : ObjectId("5ef1f1b9a75023095e995d9f"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100), 
    "author" : {
        "name" : "WangPlus"
        "contacts" : [
            {
                "mobile" : "13800000000"
            }, 
            {
                "mobile" : "13811111111"
            }
        ]
    }, 
    "tag" : "CSharp"
    "post_time" : ISODate("2020-06-23T12:12:40.463+0000")
}

對比代碼時間和數據時間,會發現這兩個時間差了8小時 - 正好的中國的時區時間。



MongoDB規定,在數據集中存儲時間時,只會保存UTC時間。


如果只是保存(像上邊這樣),或者查詢時使用時間作為條件(例如查詢post_time < DateTime.Now的數據)時,是可以使用的,不會出現問題。


但是,如果是查詢結果中有時間字段,那這個字段,會被DateTime默認設置為DateTimeKind.Unspecified類型。而這個類型,是無時區信息的,輸出显示時,會造成混亂。


為了避免這種情況,在進行時間字段的映射時,需要加上屬性:


[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime post_time { get; set; }

這樣做,會強制DateTime類型的字段為DateTimeKind.Local類型。這時候,從显示到使用就正確了。



但是,別高興的太早,這兒還有一個但是。


這個但是是這樣的:數據集中存放的是UTC時間,跟我們正常的時間有8小時時差,如果我們需要按日統計,比方每天的銷售額/點擊量,怎麼搞?上面的方式,解決不了。


當然,基於MongoDB自由的字段處理,可以把需要統計的字段,按年月日時分秒拆開存放,像下面這樣的:


class Post_Time
{

    public int year { get; set; }
    public int month { get; set; }
    public int day { get; set; }
    public int hour { get; set; }
    public int minute { get; set; }
    public int second { get; set; }
}

能解決,但是Low哭了有沒有?



下面,終極方案來了。它就是:改寫MongoDB中對於DateTime字段的序列化類。噹噹當~~~


先創建一個類MyDateTimeSerializer


public class MyDateTimeSerializer : DateTimeSerializer
{
    public override DateTime Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    
{
        var obj = base.Deserialize(context, args);
        return new DateTime(obj.Ticks, DateTimeKind.Unspecified);
    }
    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DateTime value)
    
{
        var utcValue = new DateTime(value.Ticks, DateTimeKind.Utc);
        base.Serialize(context, args, utcValue);
    }
}

代碼簡單,一看就懂。


注意,使用這個方法,上邊那個對於時間加的屬性[BsonDateTimeOptions(Kind = DateTimeKind.Local)]一定不要添加,要不然就等着哭吧:P


創建完了,怎麼用?


如果你只想對某個特定映射的特定字段使用,比方只對CollectionModelpost_time字段來使用,可以這麼寫:


[BsonSerializer(typeof(MyDateTimeSerializer))]
public DateTime post_time { get; set; }

或者全局使用:


BsonSerializer.RegisterSerializer(typeof(DateTime), new MongoDBDateTimeSerializer());

BsonSerializer是MongoDB.Driver的全局對象。所以這個代碼,可以放到使用數據庫前的任何地方。例如在Demo中,我放在Main里了:


static async Task Main(string[] args)
{
    BsonSerializer.RegisterSerializer(typeof(DateTime), new MyDateTimeSerializer());

    await Demo();
    Console.ReadKey();
}

這回看數據,數據集中的post_time跟當前時間显示完全一樣了,你統計,你分組,可以隨便霍霍了。


7. Dictionary字段


這個需求很奇怪。我們希望在一個Key-Value的文檔中,保存一個Key-Value的數據。但這個需求又是真實存在的,比方保存一個用戶的標籤和標籤對應的命中次數。


數據聲明很簡單:


public Dictionary<stringint> extra_info { get; set; }

MongoDB定義了三種保存屬性:DocumentArrayOfDocumentsArrayOfArrays,默認是Document


屬性寫法是這樣的:


[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)]
public Dictionary<stringint> extra_info { get; set; }

這三種屬性下,保存在數據集中的數據結構有區別。


DictionaryRepresentation.Document



    "extra_info" : {
        "type" : NumberInt(1), 
        "mode" : NumberInt(2)
    }
}

DictionaryRepresentation.ArrayOfDocuments



    "extra_info" : [
        {
            "k" : "type"
            "v" : NumberInt(1)
        }, 
        {
            "k" : "mode"
            "v" : NumberInt(2)
        }
    ]
}

DictionaryRepresentation.ArrayOfArrays



    "extra_info" : [
        [
            "type"
            NumberInt(1)
        ], 
        [
            "mode"
            NumberInt(2)
        ]
    ]
}

這三種方式,從數據保存上並沒有什麼區別,但從查詢來講,如果這個字段需要進行查詢,那三種方式區別很大。


如果採用BsonDocument方式查詢,DictionaryRepresentation.Document無疑是寫着最方便的。


如果用Builder方式查詢,DictionaryRepresentation.ArrayOfDocuments是最容易寫的。


DictionaryRepresentation.ArrayOfArrays就算了。數組套數組,查詢條件寫死人。


我自己在使用時,多數情況用DictionaryRepresentation.ArrayOfDocuments


五、其它映射屬性


上一章介紹了數據映射的完整內容。除了這些內容,MongoDB還給出了一些映射屬性,供大家看心情使用。


1. BsonElement屬性


這個屬性是用來改數據集中的字段名稱用的。


看代碼:


[BsonElement("pt")]
public DateTime post_time { get; set; }

在不加BsonElement的情況下,通過數據映射寫到數據集中的文檔,字段名就是變量名,上面這個例子,字段名就是post_time


加上BsonElement后,數據集中的字段名會變為pt


2. BsonDefaultValue屬性


看名稱就知道,這是用來設置字段的默認值的。


看代碼:


[BsonDefaultValue("This is a default title")]
public string title { get; set; }

當寫入的時候,如果映射中不傳入值,則數據庫會把這個默認值存到數據集中。


3. BsonRepresentation屬性


這個屬性是用來在映射類中的數據類型和數據集中的數據類型做轉換的。


看代碼:


[BsonRepresentation(BsonType.String)]
public int favor { get; set; }

這段代表表示,在映射類中,favor字段是int類型的,而存到數據集中,會保存為string類型。


前邊Decimal轉換和枚舉轉換,就是用的這個屬性。


4. BsonIgnore屬性


這個屬性用來忽略某些字段。忽略的意思是:映射類中某些字段,不希望被保存到數據集中。


看代碼:


[BsonIgnore]
public string ignore_string { get; set; }

這樣,在保存數據時,字段ignore_string就不會被保存到數據集中。


六、總結


數據映射本身沒什麼新鮮的內容,但在MongoDB中,如果用好了映射,開發過程從效率到爽的程度,都不是SQL可以相比的。正所謂:


一入Mongo深似海,從此SQL是路人。



謝謝大家!


(全文完)



本文的配套代碼在https://github.com/humornif/Demo-Code/tree/master/0015/demo


 


 









微信公眾號:老王Plus

掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此聲明和原文鏈接

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理



【其他文章推薦】



網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!



網頁設計公司推薦不同的風格,搶佔消費者視覺第一線



※Google地圖已可更新顯示潭子電動車充電站設置地點!!



※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益



※別再煩惱如何寫文案,掌握八大原則!



Orignal From: MongoDB via Dotnet Core數據映射詳解

留言

這個網誌中的熱門文章

掃地機器人可以隨身帶上飛機嗎?我想要拿去送給國外的朋友

掃地機器人如果要隨身戴上飛機需要滿足兩個條件: 一個是掃地機器人連同你的隨身行李,整體的體積和重量要符合上機條件,這個具體每家航空公司都不同,可以諮詢,簡單的說就是隨身行李不要超寬超重。 還有一個就是由於掃地機器人內置了鋰電池,所以內置電池的容量要符合相關規定,每個掃地機器人電池容量都不同,具體自行查詢。 根據民航的相關安全要求,凡帶有鋰電池的電子設備均不可以托運,但符合重量要求,尺寸要求以及電量要求的鋰電池及其設備是可以帶上飛機的。 《鋰電池航空運輸規範》中內含鋰離子電池的設備電池額定能量不應超過100Wh的規定,符合國標GB31241-2014,並通過UN38.3航空運輸認證等國際安全標準,所以可以帶上飛機。但是不能托運,只能隨身攜帶。 掃地機器人     掃地機器人     掃地機器人吸塵器 http://www.greenh3y.com/?p=400 Orignal From: 掃地機器人可以隨身帶上飛機嗎?我想要拿去送給國外的朋友

不滿國際規範斷財路 非洲多國擬退野生動保公約

摘錄自2019年09月01日中央通訊社非洲報導 非洲南部多國揚言退出「瀕臨絕種野生動植物國際貿易公約」,因為公約多數成員拒絕放寬象牙與犀牛角交易,並且幾乎全面禁止將野生捕獲的大象送到動物園。 這個公約嚴格規範全球野生動物交易,包括限制象牙與犀牛角交易。 本週在日內瓦召開修訂「瀕臨絕種野生動植物國際貿易公約」(CITES)的會議期間,由於區域集團非南開發共同體(SADC)的多項提案遭否決,這個集團與公約的關係惡化。 全球大象數量最多的區域波札那、納米比亞與辛巴威要求販售取自自然死亡、充公與汰除的大象象牙,這項提議被居多數的101票否決。 40多年前制訂的CITES規範約3萬6000種動植物交易,並設計有助於遏止非法交易和制裁違規國家的機制。 不過有16個成員國的非南開發共同體部分會員批評CITES對非洲國家的問題視若無睹。 坦尚尼亞環境部長西蒙巴徹恩(George Simbachawene)於日內瓦召開的會議中表示:「結果無法採取進步、公平、包容與基於科學的的保育策略。」 他說:「該是認真重新考慮我們加入CITES是否有任何實質益處的時候了。」 本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理 【搬家相關資訊指南】 台中搬家 , 彰化搬家 , 南投搬家 前需注意的眉眉角角,別等搬了再說! 避免吃悶虧無故遭抬價! 台中搬家公司 免費估價,有契約讓您安心有保障! 評比 彰化搬家公司費用 , 南投搬家公司費用 收費行情懶人包大公開 彰化搬家費用 , 南投搬家費用 ,距離,噸數怎麼算?達人教你簡易估價知識! Orignal From: 不滿國際規範斷財路 非洲多國擬退野生動保公約

全球第一國 帛琉立法禁用、禁售防曬乳

摘錄自2018年11月2日蘋果日報帛琉報導 為了保護珊瑚礁生態,帛琉政府昨(1)日表示已立法嚴禁販售並使用防曬乳,此法將於2020年1月1日起正式生效。帛琉也成為全球首個全面禁止防曬乳的國家。 帛琉國會上周通過此法案,全面禁止使用和販售含有10種有害化學物質的防曬乳,違者將被處以1000美元(約3萬783元台幣)罰款。若遊客被發現私帶防曬乳入境,也會遭到沒收。帛琉總統雷蒙傑索(Tommy Remengesau)說:「沒收(防曬乳)已經足夠讓人不進行商業使用,而這也是很聰明的一招,一方面教育觀光客,又不會把他們嚇跑。」 根據官方說法,帛琉的熱門潛水點每小時會有4艘載著觀光客的船隻造訪,他們身上的防曬乳化學物質相當可觀。總統府發言人說:「帛琉各潛水和浮潛地點每天都有好幾加崙的防曬乳入海。我們只是盡力要防止環境遭受污染。」 本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理 【其他文章推薦】 ※ 台中搬家 , 彰化搬家 , 南投搬家 前需注意的眉眉角角,別等搬了再說! ※在找尋 搬家 公司嗎? ※搬家不受騙不被宰 桃園搬家公司 , 桃園市搬家公司 公開報價讓你比價不吃虧! Orignal From: 全球第一國 帛琉立法禁用、禁售防曬乳