標籤: 台中網頁設計公司推薦

  • 在開發框架中擴展微軟企業庫,支持使用ODP.NET(Oracle.ManagedDataAccess.dll)訪問Oracle數據庫,基於Enterprise Library的Winform開發框架實現支持國產達夢數據庫的擴展操作

    在開發框架中擴展微軟企業庫,支持使用ODP.NET(Oracle.ManagedDataAccess.dll)訪問Oracle數據庫,基於Enterprise Library的Winform開發框架實現支持國產達夢數據庫的擴展操作

    在前面隨筆《》中介紹了在代碼生成工具中使用ODP.NET(Oracle.ManagedDataAccess.dll)訪問Oracle數據庫,如果我們在框架應用中需要使用這個如何處理了?由於我們開發框架底層主要使用微軟企業庫(目前用的版本是4.1),如果是使用它官方的Oracle擴展,那麼就是使用EntLibContrib.Data.OdpNet(這個企業庫擴展類庫使用了Oracle.DataAccess.dll),不過這種方式還是受限於32位和64位的問題;假如我們使用ODP.NET(Oracle.ManagedDataAccess.dll)方式,可以使用自己擴展企業庫支持即可,類似於我們支持國產數據庫–達夢數據庫一樣的原理,使用Oracle.ManagedDataAccess類庫可以避免32位和64位衝突問題,實現統一兼容。

    1、擴展支持ODP.NET(Oracle.ManagedDataAccess.dll)訪問

    為了實現自定義的擴展支持,我們需要對企業庫的擴展類庫進行處理,類似我們之前編寫達夢數據庫的自定義擴展類庫一樣,這方面可以了解下之前的隨筆《》,我們現在增加對ODP.NET(Oracle.ManagedDataAccess.dll)方式的擴展支持。

    首先我們創建一個項目,並通過Nugget的方式獲得對應的Oracle.ManagedDataAccess.dll類庫,參考企業庫對於Mysql的擴展或者其他的擴展,稍作調整即可。

     OracleDatabase類似下面代碼

    using System;
    using System.Data;
    using System.Data.Common;
    
    using Microsoft.Practices.EnterpriseLibrary.Common;
    using Microsoft.Practices.EnterpriseLibrary.Data;
    using Microsoft.Practices.EnterpriseLibrary.Data.Configuration;
    using Oracle.ManagedDataAccess.Client;
    
    namespace EntLibContrib.Data.OracleManaged
    {
        /// <summary>
        /// <para>Oracle數據庫對象(使用ODP驅動)</para>
        /// </summary>
        /// <remarks>
        /// <para>
        /// Internally uses OracleProvider from Oracle to connect to the database.
        /// </para>
        /// </remarks>
        [DatabaseAssembler(typeof(OracleDatabaseAssembler))]
        public class OracleDatabase : Database
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="OracleDatabase"/> class
            /// with a connection string.
            /// </summary>
            /// <param name="connectionString">The connection string.</param>
            public OracleDatabase(string connectionString) : base(connectionString, OracleClientFactory.Instance)
            {
            }
            
            /// <summary>
            /// <para>
            /// Gets the parameter token used to delimit parameters for the
            /// Oracle database.</para>
            /// </summary>
            /// <value>
            /// <para>The '?' symbol.</para>
            /// </value>
            protected char ParameterToken
            {
                get
                {
                    return ':';
                }
            }
    
            .........

    主要就是把對應的類型修改為Oracle的即可,如Oracle的名稱,以及參數的符號為 :等地方,其他的一一調整即可,不在贅述。

    完成后,修改程序集名稱,編譯為 EntLibContrib.Data.OracleManaged.dll 即可。

     

    2、框架應用的數據庫配置項設置

    完成上面的步驟,我們就可以在配置文件中增加配置信息如下所示,它就能正常的解析並處理了。

     

     上面使用了兩種方式,一種是官方擴展的EntLibContrib.Data.OdpNet方式,一種是我們這裏剛剛出爐的 EntLibContrib.Data.OracleManaged方式,完整的數據庫支持文件信息如下所示。

    <?xml version="1.0"?>
    <configuration>
      <configSections>
        <section name="dataConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
        <section name="oracleConnectionSettings" type="EntLibContrib.Data.OdpNet.Configuration.OracleConnectionSettings, EntLibContrib.Data.OdpNet" />
      </configSections>
      <connectionStrings>
        <!--SQLServer數據庫的連接字符串-->
        <add name="sqlserver" providerName="System.Data.SqlClient" connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=WinFramework;Integrated Security=SSPI"/>
        
        <!--Oracle數據庫的連接字符串-->
        <add name="oracle" providerName="System.Data.OracleClient" connectionString="Data Source=orcl;User ID=whc;Password=whc"/>
        
        <!--MySQL數據庫的連接字符串-->
        <add name="mysql" providerName="MySql.Data.MySqlClient" connectionString="Server=localhost;Database=WinFramework;Uid=root;Pwd=123456;"/>
        
        <!--PostgreSQL數據庫的連接字符串-->
        <add name="npgsql" providerName="Npgsql" connectionString="Server=localhost;Port=5432;Database=postgres;User Id=postgres;Password=123456"/>
        
        <!--路徑符號|DataDirectory|代表當前運行目錄-->    
        <!--SQLite數據庫的連接字符串-->
        <add name="sqlite"  providerName="System.Data.SQLite" connectionString="Data Source=|DataDirectory|\WinFramework.db;Version=3;" />
        <!--Microsoft Access數據庫的連接字符串-->
        <add name="access" providerName="System.Data.OleDb" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\WinFramework.mdb;User ID=Admin;Jet OLEDB:Database Password=;" />   
        
        <!--IBM DB2數據庫的連接字符串-->
        <add    name="db2" providerName="IBM.Data.DB2"    connectionString="database=whc;uid=whc;pwd=123456"/>
        
        <!--採用OdpNet方式的Oracle數據庫的連接字符串-->
        <add    name="oracle2"    providerName="Oracle.DataAccess.Client"    connectionString="Data Source=orcl;User id=win;Password=win;" />
        <add    name="oracle3"    providerName="OracleManaged"    connectionString="Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl.mshome.net)));User ID=win;Password=win" />
      </connectionStrings>
      <dataConfiguration defaultDatabase="oracle3">
        <providerMappings>
          <add databaseType="EntLibContrib.Data.MySql.MySqlDatabase, EntLibContrib.Data.MySql" name="MySql.Data.MySqlClient" />
          <add databaseType="EntLibContrib.Data.SQLite.SQLiteDatabase, EntLibContrib.Data.SqLite" name="System.Data.SQLite" />
          <add databaseType="EntLibContrib.Data.PostgreSql.NpgsqlDatabase, EntLibContrib.Data.PostgreSql" name="Npgsql" />      
          <add databaseType="EntLibContrib.Data.DB2.DB2Database, EntLibContrib.Data.DB2" name="IBM.Data.DB2" />
          <add databaseType="EntLibContrib.Data.OdpNet.OracleDatabase, EntLibContrib.Data.OdpNet" name="Oracle.DataAccess.Client" />
          <add databaseType="EntLibContrib.Data.Dm.DmDatabase, EntLibContrib.Data.Dm" name="Dm" />
          <!--增加ODP.NET(Oracle.ManagedDataAccess.dll)方式的擴展支持-->
          <add databaseType="EntLibContrib.Data.OracleManaged.OracleDatabase, EntLibContrib.Data.OracleManaged" name="OracleManaged" />
        </providerMappings>
      </dataConfiguration>
      
      <appSettings>
    
      </appSettings>
      <startup useLegacyV2RuntimeActivationPolicy="true">
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
        <supportedRuntime version="v2.0.50727"/>
      </startup>
    </configuration>

    這樣我們底層就可以實現多種數據庫的兼容訪問了。

    採用不同的數據庫,我們需要為不同數據庫的訪問層進行生成處理,如為SQLServer數據的表生成相關的數據訪問層DALSQL,裏面放置各個表對象的內容,不過由於採用了相關的繼承類處理和基於數據庫的代碼生成,需要調整的代碼很少。

    我們來編寫一段簡單的程序代碼來測試支持這種ODP.net方式,測試代碼如下所示。

    private void btnGetData_Click(object sender, EventArgs e)
    {
        string sql = "select * from T_Customer";// + " Where Name = :name";
        Database db = DatabaseFactory.CreateDatabase();
        DbCommand command = db.GetSqlStringCommand(sql);
        //command.Parameters.Add(new OracleParameter("name", "張三"));
    
        using (var ds = db.ExecuteDataSet(command))
        {
            this.dataGridView1.DataSource = ds.Tables[0];   
        }
    }

    測試界面效果如下所示。

    以上這些處理,可以適用於Web框架、Bootstrap開發框架、Winform開發框架、混合式開發框架中的應用,也就是CS、BS都可以使用。

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

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 人機對話技術研究進展與思考

    人機對話技術研究進展與思考

    嘉賓:袁彩霞 博士 北京郵電大學 副教授

    整理:Hoh Xil

    來源:阿里小蜜 & DataFun AI Talk

    出品:DataFun

    注:歡迎轉載,轉載請在留言區內留言。

    導讀:本次分享的主題為人機對話技術研究進展與思考。主要梳理了我們團隊近兩年的工作,渴望可以通過這樣的介紹,能給大家一個關於人機對話 ( 包括它的科學問題和應用技術 ) 方面的啟示,幫助我們進行更深入的研究和討論。主要包括:

    1. Spoken dialogue system:a bird view ( 首先我們來看什麼是人機對話,尤其是 Spoken dialogue。其實說 Spoken 的時候,有兩層含義:第一個 spoken 就是 speech,第二個我們處理的語言本身具有 spoken 的特性。但是,稍後會講的 spoken 是指我們已經進行語音識別之後,轉換為文本的一個特殊的自然語言,後面討論的口語對話不過多地討論它的口語特性,主要是講人和機器之間的自然語言對話。)

    2. X-driven dialogue system:緊接着來講解我們近些年的研究主線 X-driven dialogue syatem,X 指構建一個對話系統時,所採用的數據是什麼,從最早的 dialogue -> FAQ -> KB -> KG -> document 以及我們一直在嘗試的圖文多模態數據。

    3. Concluding remarks ( 結束語 )

    01

    Spoken dialogue system:a bird view

    學術界關於對話系統有着不同的劃分,這種劃分目前看來不是非常準確,也不是特別標準的劃分了。但是,接下來的內容,主要是圍繞着這兩個主線:

    限定領域,專門指任務型對話 ( 圍繞某一特定用戶對話目標而展開的 )。對於任務型對話,對話系統的優化目標就是如何以一個特別高的回報、特別少的對話輪次、特別高的成功率來達成用戶的對話目標。所以即便是限定領域,我們這裏討論的也是特別限定的、專門有明確的用戶對話目標的一種對話。

    開放領域,not purely task-oriented, 已經不再是純粹的對話目標驅動的對話,包括:閑聊、推薦、信息服務等等,後面逐步展開介紹。

    我們在研究一個問題或者做論文答辯和開題報告時,經常討論研究對象的意義在哪裡。圖中,前面講的是應用意義,後面是理論意義。我們實驗室在北京郵電大學叫智能科學與技術實驗室,其實她的前身叫人工智能實驗室。所以從名字來看,我們做了非常多的 AI 基礎理論的研究,我們在研究這些理論的時候,也會講 AI 的終極目的是研製一種能夠從事人類思維活動的計算機系統。人類思維活動建立在獲取到的信號的基礎上。人類獲取信號的方式大體有五類,包括視覺、聽覺、觸覺、味覺、嗅覺等,其中視覺和聽覺是兩個比較高級的傳感器通道,尤其是視覺通道,佔據了人類獲得信息的80%以上。所以我們從這兩個角度,設立了兩個研究對象:第一個是語言,第二個是圖像。而我們在研究語言的時候,發現語言有一個重要的屬性,叫交互性,交互性最典型的一個體現就是對話;同時,語言不是一個獨立的模態,語言的處理離不開跟它相關的另一個通道,就是視覺通道。所以我們早期更多是為了把交互和多模態這樣的屬性納入到語言建模的範圍,以其提升其它自然語言處理系統的性能,這就是我們研究的一個動機。

    1. Block diagram

    上圖為 CMU 等在1997年提出來的人機對話框架,基於這個框架人們開發出了非常多優秀的應用系統,比如應用天氣領域的 “Jupiter”。這個框架從提出到商業化應用,一直到今天,我們都還沿着這樣的一個系統架構在進行開發,尤其是任務驅動的對話。

    這就是具體的對話系統的技術架構。

    1. Specific domain

    這個架構發展到現在,在功能模塊上,已經有了一個很清晰的劃分:

    首先進行語音識別,然後自然語言理解,緊接着做對話管理,將對話管理的輸出交給自然語言生成模塊,最後形成自然語言應答返回給用戶。這就是一個最典型的 specific domain 的架構。早期 task 限定的 dialogue,基本上都是按照這個架構來做的。這個架構雖然是一個 Pipeline,但是從研究的角度來講,每一個模塊和其它模塊之間都會存在依賴關係。因此,我們試圖從研究的角度把不同的功能模塊進行統一建模。在這個建模過程中,又會產生新的學術性問題,我們旨在在這樣的問題上可以產生驅動性的技術。

    1. Open domain

    Open domain,也就是“閑聊”,實現上主要分為途徑:

    第一個是基於匹配/規則的閑聊系統;第二個是基於檢索的閑聊系統;第三個是基於編解碼結構的端到端對話系統。當然,實際情境中,這幾個途徑往往結合在一起使用。

    02

    X-Driven dialogue system

    目前無論是任務型對話還是閑聊式對話,都採用數據驅動的方法,因此依據在構建人機對話系統時所用到的數據不同,建模技術和系統特性也就體現出巨大的不同。我們把使用的數據記為 X,於是就有了不同的 X 驅動的對話。

    1. Our roadmap

    如果想讓機器學會像人一樣對話,我們可以提供的最自然的數據就是 dialogue。我們從2003年開始做對話驅動的對話;2012年開始做 FAQ 驅動的對話;2015年開始做知識庫 ( KB ) 驅動的對話;2016年開始做知識圖譜 ( KG ) 驅動的對話,相比於 KB,KG 中的知識點產生了關聯,有了這種關聯人們就可以在大規模的圖譜上做知識推理;2017年開始做文檔驅動的對話。這就是我們研究的大致脈絡。

    1. Dialogue-driven dialogue

    早期在做 Dialogue driven 的時候,多依賴人工採集數據,但是,從2013年以來,逐步開放了豐富的涵蓋多領域多場景的公開數據集。比如最近的 MultiWOZ,從 task specific 角度講,數據質量足夠好、數據規模足夠大,同時涵蓋的對話情景也非常豐富。但是,目前公開的中文數據集還不是很多。

    這個是和任務型對話無關的數據集,也就是採集的人與人對話的數據集。尤其以 Ubuntu 為例,從15年更新至今,已經積累了非常大規模的數據。

    以 Dialogue 為輸入,我們開展了任務型和非任務型兩個方向的工作。先來看下任務型對話:

    2.1 NLU

    當一個用戶輸入過來,第一個要做的就是自然語言理解 ( NLU ),NLU 要做的三件事為:Domain 識別;Intent 識別;信息槽識別或叫槽填充。這三個任務可以分別獨立地或採用管道式方法做,也可以聯合起來進行建模。在聯合建模以外,我們還做了一些特別的研究。比如我們在槽識別的時候,總是有新槽,再比如有些槽值非常奇怪,例如 “XX手機可以一邊打電話一邊視頻嗎?”,對應着槽值 “視頻電話”,採用序列標註的方式,很難識別它,因為這個槽值非常不規範。用戶輸入可能像這樣語義非常鬆散,不連續,也可能存在非常多噪音,在進行聯合建模時,傳統的序列標註或分類為思想,在實際應用中已經不足以解決問題了。

    我們針對這個問題做了比較多的探討,右圖為我們2015年的一個工作:在這三個任務聯合建模的同時,在槽填充這個任務上將序列標註和分類進行同時建模,來更好地完成 NLU。

    在 NLU 領域還有一個非常重要的問題,隨着開發的業務領域越來越多,我們發現多領域對話產生了諸多非常重要的問題,例如在數據層有些 domain 數據可能很多,有些 domain 數據可能很少,甚至沒有,於是就遇到冷啟動的問題。因此,我們做了非常多的 domain transfer 的工作。上圖為我們2016年的一個工作,我們會把數據比較多的看成源領域,數據比較少的看成目標領域。於是,嘗試了基於多種遷移學習的 NLU,有的是在特徵層進行遷移,有的是在數據層遷移,有的是在模型層進行遷移。圖中是兩個典型的在特徵層進行遷移的例子,不僅關注領域一般特徵,而且關注領域專門特徵,同時採用了對抗網絡來生成一個虛擬的特徵集的模型。

    2.2 NLU+DM

    緊接着,我們研究了 NLU 和對話管理 ( DM ) 進行聯合建模,因為我們發現人人對話的時候,不見得是聽完對方說完話,理解了對方的意圖,然後才形成對話策略,有可能這兩個過程是同時發生的。甚或 DM 還可以反作用於 NLU。早期我們基於的一個假設是, NLU 可能不需要一個顯式的過程,甚至不需要一個顯式的 NLU 的功能,我們認為 NLU 最終是服務於對話管理 ( DM ),甚至就是對話管理 ( DM ) 的一部分。所以,2013年的時候,我們開始了探索,有兩位特別優秀的畢業生在這兩個方面做了特別多的工作。比如,如何更好地聯合建模語言理解的輸出和對話管理的策略優化。這是我們在 NLU 和 DM 聯合建模的工作,同時提升了 NLU 和 DM 的性能。

    在聯合模型中,我們發現,DM 的建模涉及到非常多的 DRL ( 深度強化學習 ) 的工作,訓練起來非常困難,比如如何設計一個好的用戶模擬器,基於規則的,基於統計的,基於語言模型的,基於 RL 的等等我們嘗試了非常多的辦法,也取得了一系列有趣的發現。2018年時我們研究一種不依賴於規則的用戶模擬器,業界管這個問題叫做 “Self”-play,雖然我們和 “Self”-play 在網絡結構上差異挺大的,但是我們還是借鑒了 “Self”-play 訓練的特性,把我們自己的系統叫做 “Self”-play。在這樣的機制引導下,我們研究了不依賴於規則,不依賴於有標記數據的用戶模擬器,使得這個用戶模擬器可以像 Agent 一樣,和我們所構造的對話的 Agent 進行交互,在交互的過程中完成對用戶的模擬。

    在訓練過程中還有一個重要的問題,就是 reward 怎麼來,我們知道在 task oriented 時,reward 通常是人類專家根據業務邏輯/規範制定出來的。事實上,當我們在和環境交互的時候不知道 reward 有多大,但是環境會隱式地告訴我們 reward 是多大,所以我們做了非常多的臨接對和 reward reshaping 的工作。

    2.3 小結

    Dialogue-driven dialogue 這種形式的對話系統,總結來看:

    優點:

    定義非常好,邏輯清晰,每一個模塊的輸入輸出也非常清晰,同時有特別堅實的數學模型可以對它進行建模。

    缺點:

    由於非常依賴數據,同時,不論是在 NLU 還是 NLG 時,我們都是採用有監督的模型來做的,所以它依賴於大量的、精細的標註數據。

    而 DM 往往採用 DRL 來做。NIPS2018 時的一個 talk,直接指出:任何一個 RL 都存在的問題,就是糟糕的重現性、復用性、魯棒性。

    1. FAQ-driven dialogue

    FAQ 是工業界非常常見的一種情景:有大量的標準問,以及這個標準問的答案是什麼。基於這個標準問,一個用戶的問題來了,如何找到和它相似的問題,進而把答案返回給用戶,於是這個 Service 就結束了。

    實際中,我們如何建 FAQ?更多的時候,我會把這個問題和我們庫的標準問題做一個相似度的計算或者做一個分類。

    我們在做這個工作的時候發現一個特別大的問題,就是 Unbalanced Data 問題。比如,我們有5000個問題,每個問題都有標準答案,有些問題可能對應的用戶問題特別多,比如 “屏幕碎了” 可能會有1000多種不同的問法,還有些問題,可能在幾年的時間里都沒有人問到過。所以,面對數據不均衡的問題,我們從2016年開始做了 Data transfer 的工作。

    大致的思路是:我有一個標準問題,但是很糟糕,這個標準問題沒有用戶問題,也就是沒有訓練語料。接下來發現另外一個和這個標準問很相似的其它標準問有很多的訓練語料,於是藉助這個標準問,來生成虛擬樣本,進而削弱了 Unbalance。

    具體的方法:我們把目標領域的標準問看成 Query,把和它相似的標準問題及其對應的用戶問題看成 Context,採用了 MRC 機器閱讀理解的架構來生成一個答案,作為目標問題的虛擬的用戶問題,取得了非常好的效果,並且嘗試了三種不同的生成用戶問題的方法。

    實際項目中,FAQ 中的 Q 可能有非常多的問題,例如3000多個類,需要做極限分類,這就導致性能低下,且非常耗時,不能快速響應用戶的問題。於是我們做了一個匹配和分類進行交互的 model,取得了不錯的效果。

    目前,大部分人都認為 FAQ 驅動的 dialogue 不叫 dialogue,因為我們通常說的 dialogue 輪次是大於兩輪的。而 FAQ 就是一個 QA 系統,沒有交互性。有時候帶來的用戶體驗非常不友好,比如當答案非常長的時候,系統要把長長的答案返回,就會一直講,導致用戶比較差的體驗。於是,我們基於 FAQ 發展出了一個多輪對話的數據,如右圖,這是我們正在開展的一個工作。

    1. KB-driven dialogue

    KB 最早人們認為它就是一個結構化的數據庫,通常存儲在關係型數據庫中。比如要訂一個酒店,這個酒店有各種屬性,如位置、名稱、戶型、價格、面積等等。早期 CMU 的對話系統,所有的模塊都要和 Hub 進行交互,最後 Hub 和後端的數據庫進行交互。數據庫的價值非常大,但是早期人們在建模人機對話的時候,都忽視了數據庫。這裏就會存在一個問題:機器和用戶交互了很久,而在檢索數據庫時發現沒有答案,或者答案非常多,造成用戶體驗非常糟糕。

    從2012年開始,我們開始把 KB 引入我們的對話系統。圖中的對話系統叫做 “teach-and-learn” bot,這是一個多模態的對話,但是每個涉及到的 object,我們都會把它放到 DB 中。和用戶交互過程中,不光看用戶的對話狀態,還要看數據庫狀態。這個想法把工作往前推進了一些。

    直到2016年,MSR 提出的 KB-InfoBot,第一次提出了進行數據庫操作時,要考慮它的可導性,否則,就沒辦法在 RL 網絡中像其它的 Agent action 一樣進行求導。具體的思路:把數據庫的查詢和 Belief State 一起總結起來做同一個 belief,進而在這樣的 belief 基礎上做各種對話策略的優化。

    在上述方法的基礎上,我們做了有效的改良,包括 entropy regularities 工作。是每次和用戶進行交互時,數據庫的 entropy 會發生變化。比如當機器問 “你想訂哪裡的酒店?”,用戶答 “阿里中心附近的。”,於是數據庫立刻進行了一次 entropy 計算進行更新,接着繼續問 “你想訂哪一天的?”,用戶答 “訂7月28號的”,於是又進行了一次 entropy 計算進行更新。這樣在和用戶進行交互的時候,不光看用戶的 dialogue 輸入,還看 DB 的 entropy 輸入,以這兩項共同驅動 Agent action 進行優化。

    這裏我們做了特別多的工作,信息槽從1個到5個,數據庫的規模從大到小,都做了特別多的嘗試,這樣在和用戶交互的時候,agent 可以自主的查詢檢索,甚至可以填充和修改數據庫。

    這是我們2017發布的一個工作,KB driven-dialogue,其優點:

    控制萬能高頻回復 ( 提高答應包含的有用信息 )

    賦予 agent 對話主動性

    1. KG-driven dialogue

    剛剛講的基於 KB 的 dialogue 任務,基本都認為對話任務就是在進行槽填充的任務,如果一個 agent 是主動性的,通過不停的和用戶進行交互來採集槽信息,所以叫槽填充,當槽填完了,就相當於對話任務成功了。但是,當我們在定義槽的時候,我們認為槽是互相獨立的,並且是扁平的。然而,實際中許多任務的槽之間存在相關性,有的是上下位關係,有的是約束關係,有的是遞進關係等等。這樣自然的就引出了知識圖譜,知識圖譜可以較好地描述上述的相關性。於是,產生了兩個新問題:

    知識圖譜驅動的對話理解:實體鏈接

    知識圖譜驅動的對話管理:圖路徑規劃

    這裏主要講下第二個問題。

    舉個例子,我們在辦理電信業務,開通一個家庭寬帶,需要提供相關的證件,是自己去辦,還是委託人去辦,是房東還是租戶等等,需要提供各種不同的材料。於是這個情景就產生了條件的約束,某一個 node 和其它 node 是上下位的關係,比如證件可以是身份證,也可以是護照或者戶口簿等等。所以我們可以通過知識圖譜來進行處理。

    當一個用戶的對話過來,首先會鏈接到不同的 node,再基於 node 和對話歷史構造一個對話的 state,我們會維持一個全局的 state 和一個活躍的 state,同時活躍的 state 會定義三種不同的操作動作,一個是祖先節點,一個是兄弟節點,還有一個是孩子節點。所以,在這樣的知識圖譜上如何尋優,比如當通過某種計算得到,它應該在某個節點上進行交互的時候,我們就應該輸出一個 action,這個 action 要和用戶確認他是一個租戶,還是自有住房等等。所以,這個 action 是有區別於此前所提到的在特定的、扁平的 slot 槽上和用戶進行信息的確認、修改等還是有很大不同的。解決這樣的問題,一個非常巨大的挑戰就是狀態空間非常大。比如圖中的節點大概有120個,每個節點有3個不同的狀態,知識從節點的狀態來看就有3的120次冪種可能。這也是我們正在開展的待解決的一個問題。

    在端到端的對話模型 ( 閑聊 ) 中,也開始逐步地引入知識圖譜。下面介紹兩個比較具有代表性的引入知識圖譜后的人機對話。其中右邊是2018年 IJCAI 的傑出論文,清華大學黃民烈老師團隊的工作,引入了通過 KG 來表示的 Commonsense,同時到底在編碼器端還是在解碼器端引入知識,以及如何排序,排序的時候如何結合對話的 history 做知識的推理等等,都做了特別全面的研究。

    另一個比較有代表性的工作是在 ICLR2019 提出的,在架構中引入了 Local Memory 和 Global Memory 相融合的技術,通過這種融合,在編碼器端和解碼器端同時加入了知識的推理。

    總結下 KB/KG-driven dialogue:

    優點:

    已經有大規模公開的數據 ( e.g.,InCar Assistant,MMD,M2M )。

    訓練過程可控&穩定,因為這裏多數都是有監督學習。

    缺點:

    因為採用有監督的方式進行訓練,所以存在如下問題:

    ① 環境確定性假設
    ② 缺少對動作的建模
    ③ 缺少全局的動作規劃
    Agent 被動,完全依賴於訓練數據,所以模型是不賦予 Agent 主動性的。

    構建 KB 和 KG 成本昂貴!

    1. Document-driven dialogue

    Document 驅動的對話,具有如下優點:

    ① 應用場景真實豐富:

    情景對話 ( conversation ),比如針對某個熱門事件在微博會產生很多對話,這就是一個情景式的對話。

    電信業務辦理 ( service ),比如10086有非常多的套餐,如何從中選擇一個用戶心儀的套餐?每個套餐都有說明書,我們可以圍繞套餐的說明書和用戶進行交互,如 “您希望流量多、還是語音多”,如果用戶回答 “流量多”,就可以基於文本知道給用戶推薦流量多的套餐,如果有三個候選,機器就可以基於這三個候選和用戶繼續進行交互,縮小候選套餐範圍,直到用戶選出心儀的套餐。

    電商產品推薦 ( recommendation ),可以根據商品的描述,進行各種各樣的對話。這裏的輸入不是一個 dialogue,也不是一個 KB,甚至結構化的內容非常少,完全是一個 free document,如何基於這些 document 進行推薦,是一個很好的應用場景。

    交互式信息查詢 ( retrieval ),很多時候,一次查詢的結果可能不能用戶的需求,如何基於非結構化的查詢結果和用戶進行交互,來更好地達成用戶的查詢目的。

    ……

    ② 數據獲取直接便捷:

    相比於 dialogue、FAQ、KB、KQ 等,Document 充斥着互聯網的各種各樣的文本,都可以看成是文本的數據,獲取方便,成本非常低。

    ③ 領域移植性強:

    基於文本,不再基於專家定義的 slot,也不再基於受限的 KB/KG,從技術上講,所構造的 model 本身是和領域無關的,所以它賦予了 model 很強的領域移植性。

    這是我們正在進行的工作,情景對話偏向於閑聊,沒有一個用戶目標。這裏需要解決的問題有兩個:

    如何引入文檔:編碼端引入文檔、解碼端引入文檔

    如何編碼文檔:文檔過長、冗餘信息過多

    數據:

    我們在 DoG 數據的基礎上模擬了一些數據,形成了如上圖所示的數據集,分 Blind 和 Non-blind 兩種情形構造了不同的數據集。

    我們發現,基於文本的端到端的聊天,有些是基於內容的閑聊,有些還需要回答特定的問題。所以,評價的時候,可以直接用 F1 評價回答特定問題,用閑聊通用的評價來評價基於內容的聊天。

    剛剛講的是偏閑聊式的對話,接下來講下任務型對話。

    這裏的動機分為兩種情況:單文檔和多文檔,我們目前在挑戰多文檔的情況,單文檔則採用簡單的多輪閱讀理解來做。

    多文檔要解決的問題:

    如何定義對話動作:因為是基於Document進行交互,而不再是基於slot進行交互,所以需要重新定義對話動作。

    如何建模文檔差異:以剛剛10086的例子為例,總共有10個業務,通過一輪對話,挑選出3個,這3個業務每個業務可能都有10種屬性,那麼其中一些屬性值相同的屬性,沒必要再和用戶進行交互,只需要交互它們之間不同的點,所以交互的角度需要隨對話進行動態地變化。

    數據:

    這裏採用的數據是 WikiMovies 和模擬數據,具體見上圖。

    1. A very simple sketch of dialogue

    上圖為任務型對話和非任務型對話的幾個重要節點,大家可以了解下。

    03

    Concluding remarks

    任務型對話具有最豐富的落地場景。

    純閑聊型對話系統的學術價值尚不清楚。

    任務型和非任務型邊界愈加模糊,一個典型的人人對話既包括閑聊,又包括信息獲取、推薦、服務。

    引入外部知識十分必要,外部知識形態各異,建模方法也因而千差萬別。

    Uncertainty 和 few-shot 問題,是幾乎所有的對話系統最大的 “卡脖子” 問題。

    X-driven dialogue 中的 X 還有哪些可能?剛剛提到的 X 都還是基於文本的。事實上,從2005年開始,我們已經開始做 image 和文本數據融合的對話;從2013年開始做 Visual QA/Dialogue,挑戰了 GuessWhat 和 GuessWhich 任務。

    X=multi media ( MM Dialogue ),X 還可以很寬泛的就是指多媒體,不光有 image 還可能有 text,2018年已經有了相關的任務,並且開源了非常多的電商業務。這是一個非常有挑戰性的工作,也使人機對話本身更加擬人化。

    04

    Reference

    這是部分的參考文獻,有些是我們自己的工作,有些是特別傑出的別人的工作。

    今天的分享就到這裏,感謝大家的聆聽,也感謝我們的團隊。

    歡迎關注DataFunTalk同名公眾號,收看第一手原創技術文章。

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

    【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 中國發改委協調汽車充電設施建設 加快政策落實

    中國發改委協調汽車充電設施建設 加快政策落實

    從中國發改委網站獲悉,停車場充電設施建設協調會7月18日下午召開。會議表示將針對大城市、特大城市存在的停車位資源緊張、社會停車場投資主體多、充電設施企業盈利模式相對單一等問題,進一步推進相關工作。  
      停車場充電設施建設協調會由中國國家發改委基礎產業司副司長鄭劍主持,就2016年第二批城市停車場項目配建充電基礎設施問題,與安徽、江蘇、江西、陝西、浙江、湖北、上海、大連、廈門等地方發展改革委、充電基礎設施服務企業和國家電網公司進行交流座談。   據國家能源局電力司初步統計,截至今年6月底,中國全國已建成公共充電樁8.1萬個,比去年底增長65%;隨車建成私人充電樁超過5萬個,比去年底增長約12%。1-6月全國新能源汽車充電量超過6億kWh,替代燃油約20萬噸,電動汽車的發展對能源結構調整和城市環境提升貢獻明顯。   為新能源汽車的推廣和應用創造良好的環境,國家能源局相關部門加快了推動充電樁政策規劃的落實,組織起草加快居民區充電基礎設施建設的檔。該文件有望7月份出臺,將有效推進居民區和工作場所建樁工作,合理優化公共充電樁佈局,提高公共充電樁利用率。   文章來源:中國發展網

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 特斯拉Gigafactory趕工,可望提前量產

    美國電動車商特斯拉(Tesla Motors Inc.)的平價車款「Model 3」預定2018年問世,為了趕上需求,特斯拉造價50億美元的「Gigafactory」超級電池廠正在加緊趕工,將比原定時程提前數年、明(2017)年初就可以開始量產車用鋰電池。

    華爾街日報、Electrek 24日報導,坐落於內華達州雷諾市的Gigafactory目前占地超過3,000英畝,特斯拉已加倍聘請建築工人,共計1,000名人員一週兩班每天輪流趕工,預計明年初就可大功告成。特斯拉技術長兼共同創辦人JB Straubel表示,在汽車量產前,電池和電池組的組裝廠房一定要事先完工,因此無論是建廠計畫或是電池的擴產時間表都會加快進度。

    特斯拉電池供應夥伴Panasonic Corp.已承諾要為該廠提供16億美元的資金,目前則因為找不到合適的人才而傷透腦筋。特斯拉執行長Elon Musk預估,Gigafactory完工之後,2020年將可年產105GW的電池,足以供應120萬台Model S豪華電動轎車所需,但其中有1/3將用於定置型電池儲存裝置(stationary battery storage product)。

    假如這座廠房能如期完工、擴產,則其產能將是全球現有電池廠的10倍之多,這也使得北美的鋰礦開採活動大增。

    OilPrice.com 21日報導,電池過去幾年來的需求倍數成長,鋰已成為今(2016)年來最夯的金屬、擊敗黃金,雖然最近幾個月的價格漲勢稍緩,但強勁的基本面顯示其長期前景依舊看俏。另外,在戴姆勒(Daimler)、日產汽車等業者的推波助瀾下,預估到了2016年插電式電動車銷售量(plug-in electric vehicle,簡稱PEV)有望年增62%,2017年、2018年更有望成長60%、100%。這相當於2018年會賣出60萬輛PEV。

    (本文內容由授權提供)

     

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 基於.NetStandard的簡易EventBus實現-基礎實現

    基於.NetStandard的簡易EventBus實現-基礎實現

    一、問題背景

      最近離職來到了一家新的公司,原先是在乙方工作,這回到了甲方,在這一個月中,發現目前的業務很大一部分是靠輪詢實現的,例如:通過輪詢判斷數據處於B狀態了,則輪詢到數據后執行某種動作,這個其實是非常浪費的,並且對於數據的實時性也會不怎麼友好,基於以上的情況,在某天開車堵車時候,想到了之前偶然了解過的事件總線(EventBus),對比了公司當前的場景后,覺得事件總線應該是可以滿足需求的(PS:只是我覺得這個有問題,很多人不覺得有問題),那既然想到了,那就想自己是否可以做個事件總線的輪子

    二、什麼是事件總線

      我們知道事件是由一個Publisher跟一個或多個的Subsriber組成,但是在實際的使用過程中,我們會發現,Subsriber必須知道Publisher是誰才可以註冊事件,進而達到目的,那這其實就是一種耦合,為了解決這個問題,就出現了事件總線的模式,事件總線允許不同的模塊之間進行彼此通信而又不需要相互依賴,如下圖所示,通過EventBus,讓Publisher以及Subsriber都只需要對事件源(EventData)進行關注,不用管Publisher是誰,那麼EventBus主要是做了一些什麼事呢?

    三、EventBus做了什麼事?

      1、EventBus實現了對於事件的註冊以及取消註冊的管理

      2、EventBus內部維護了一份事件源與事件處理程序的對應關係,並且通過這個對應關係在事件發布的時候可以找到對應的處理程序去執行

      3、EventBus應該要支持默認就註冊事件源與處理程序的關係,而不需要開發人員手動去註冊(這裏可以讓開發人員去控制自動還是手動)

    四、具體實現思路

       首先在事件總線中,存在註冊、取消註冊以及觸發事件這三種行為,所以我們可以將這三種行為抽象一個接口出來,最終的接口代碼如下:

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace MEventBus.Core
    {
        public interface IEventBus
        {
            #region 接口註冊
            void Register<TEventData>(Type handlerType) where TEventData : IEventData;
            void Register(Type eventType, Type handlerType);
            void Register(string eventType, Type handlerType);
            #endregion
    
            #region 接口取消註冊
            void Unregister<TEventData>(Type handler) where TEventData : IEventData;
            void Unregister(Type eventType, Type handlerType);
            void Unregister(string eventType, Type handlerType);
            #endregion
    
    
            void Trigger(string pubKey, IEventData eventData);
            Task TriggerAsync(string pubKey, IEventData eventData);
            Task TriggerAsync<TEventData>(TEventData eventData) where TEventData : IEventData;
            void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData;
        }
    }
    

      在以上代碼中發現有些方法是有IEventData約束的,這邊IEventData就是約束入參行為,原則上規定,每次觸發的EventData都需要繼承IEventData,而註冊的行為也是直接跟入參類型相關,具體代碼如下:

    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace MEventBus.Core
    {
        public interface IEventData
        {
            string Id { get; set; }
            DateTime EventTime { get; set; }
            object EventSource { get; set; }
        }
    }
    

      接下來我們看下具體的實現代碼

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace MEventBus.Core
    {
        public class EventBus : IEventBus
        {
            private static ConcurrentDictionary<string, List<Type>> dicEvent = new ConcurrentDictionary<string, List<Type>>();
            private IResolve _iresolve { get; set; }
            public EventBus(IResolve resolve)
            {
                _iresolve = resolve;
                InitRegister();
            }
    
            public void InitRegister()
            {
                if (dicEvent.Count > 0)
                {
                    return;
                }
                //_iresolve = ioc_container;
                dicEvent = new ConcurrentDictionary<string, List<Type>>();
                //自動掃描類型並且註冊
                foreach (var file in Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll"))
                {
                    var ass = Assembly.LoadFrom(file);
                    foreach (var item in ass.GetTypes().Where(p => p.GetInterfaces().Contains(typeof(IEventHandler))))
                    {
                        if (item.IsClass)
                        {
                            foreach (var item1 in item.GetInterfaces())
                            {
                                foreach (var item2 in item1.GetGenericArguments())
                                {
                                    if (item2.GetInterfaces().Contains(typeof(IEventData)))
                                    {
                                        Register(item2, item);
                                    }
                                }
                            }
                        }
                    }
                }
            }
            //註冊以及取消註冊的時候需要加鎖處理
            private static readonly object obj = new object();
    
            #region 註冊事件
            public void Register<TEventData>(Type handlerType) where TEventData : IEventData
            {
                //將數據存儲到mapDic
                var dataType = typeof(TEventData).FullName;
                Register(dataType, handlerType);
            }
            public void Register(Type eventType, Type handlerType)
            {
                var dataType = eventType.FullName;
                Register(dataType, handlerType);
            }
            public void Register(string pubKey, Type handlerType)
            {
                lock (obj)
                {
                    //將數據存儲到dicEvent
                    if (dicEvent.Keys.Contains(pubKey) == false)
                    {
                        dicEvent[pubKey] = new List<Type>();
                    }
                    if (dicEvent[pubKey].Exists(p => p.GetType() == handlerType) == false)
                    {
                        //IEventHandler obj = Activator.CreateInstance(handlerType) as IEventHandler;
                        dicEvent[pubKey].Add(handlerType);
                    }
                }
            }
    
    
    
            #endregion
    
            #region 取消事件註冊
            public void Unregister<TEventData>(Type handler) where TEventData : IEventData
            {
                var dataType = typeof(TEventData);
                Unregister(dataType, handler);
            }
    
            public void Unregister(Type eventType, Type handlerType)
            {
                string _key = eventType.FullName;
                Unregister(_key, handlerType);
            }
            public void Unregister(string eventType, Type handlerType)
            {
                lock (obj)
                {
                    if (dicEvent.Keys.Contains(eventType))
                    {
                        if (dicEvent[eventType].Exists(p => p.GetType() == handlerType))
                        {
                            dicEvent[eventType].Remove(dicEvent[eventType].Find(p => p.GetType() == handlerType));
                        }
                    }
                }
            }
            #endregion
    
            #region Trigger觸發
            //trigger時候需要記錄到數據庫
            public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData
            {
                var dataType = eventData.GetType().FullName;
                //獲取當前的EventData綁定的所有Handler
                Notify(dataType, eventData);
            }
    
            public void Trigger(string pubKey, IEventData eventData)
            {
                //獲取當前的EventData綁定的所有Handler
                Notify(pubKey, eventData);
            }
            public async Task TriggerAsync<TEventData>(TEventData eventData) where TEventData : IEventData
            {
                await Task.Factory.StartNew(new Action(()=> 
                {
                    var dataType = eventData.GetType().FullName;
                    Notify(dataType, eventData);
                }));
            }
            public async Task TriggerAsync(string pubKey, IEventData eventData)
            {
                await Task.Factory.StartNew(new Action(() =>
                {
                    var dataType = eventData.GetType().FullName;
                    Notify(pubKey, eventData);
                }));
            }
            //通知每成功執行一個就需要記錄到數據庫
            private void Notify<TEventData>(string eventType, TEventData eventData) where TEventData : IEventData
            {
                //獲取當前的EventData綁定的所有Handler
                var handlerTypes = dicEvent[eventType];
                foreach (var handlerType in handlerTypes)
                {
                    var resolveObj = _iresolve.Resolve(handlerType);
                    IEventHandler<TEventData> handler = resolveObj as IEventHandler<TEventData>;
                    handler.Handle(eventData);
    
                }
            }
            #endregion
        }
    }
    

      代碼說明:

      1、如上的EventBus是繼承了IEventBus后的具體實現,小夥伴可能看到在構造函數里,有一個接口參數IResolve,這個主要是為了將解析的過程進行解耦,由於在一些WebApi的項目中,更加多的是使用IOC的機制進行對象的創建,那基於IResolve就可以實現不同的對象創建方式(內置的是通過反射實現)

      2、InitRegister方法通過遍歷當前目錄下的dll文件,去尋找所有實現了IEventHandler<IEventData>接口的信息,並且自動註冊到EventBus中,所以在實際使用過程中,應該是沒有機會去適用register註冊的

      3、觸發機制實現了同步以及異步的調用,這個從方法命名中就可以看出來

    五、程序Demo

      TestHandler2(繼承IEventHandler)

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading;
    using System.Windows.Forms;
    using MEventBus.Core;
    
    namespace MEventBusHandler.Test
    {
        public class TestHandler2 : IEventHandler<TestEventData>
        {
            public void Handle(TestEventData eventData)
            {
                Thread.Sleep(2000);
                MessageBox.Show(eventData.EventTime.ToString());
            }
        }
    }
    

      TestEventData(繼承EventData,EventData是繼承了IEventData的代碼)

    using MEventBus.Core;
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace MEventBusHandler.Test
    {
        public class TestEventData : EventData
        { }
    }
    

      調用代碼

    using MEventBus.Core;
    using MEventBusHandler.Test;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    
    namespace MEventBus.Test
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
                TestHandler.OnOut += TestHandler_OnOut;
            }
    
            private void TestHandler_OnOut(object sender, EventArgs e)
            {
                MessageBox.Show("Hello World");
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
                var task = new MEventBus.Core.EventBus(new ReflectResolve()).TriggerAsync(new TestEventData());
                task.ContinueWith((obj) => {
                    MessageBox.Show("事情全部做完");
                });
            }
    
            private void button2_Click(object sender, EventArgs e)
            {
               new EventBus(new ReflectResolve()).Trigger(new TestEventData());
            }
        }
    
    
    }
    

      執行結果

     

     

     

     

     我在真正的Demo中,其實是註冊了2個handler,可以在後續公布的項目地址里看到

    六、總結

      從有這個想法開始,到最終實現這個事件總線,大概總共花了2,3天的時間(PS:晚上回家獨自默默幹活),目前只能說是有一個初步可以使用的版本,並且還存在着一些問題:

      1、在.NetFrameWork下(目前公司還不想升級到.NetCore,吐血。。),如果使用AutoFac創建EventBus(單例模式下),如果Handler也使用AutoFac進行創建,會出現要麼對象創建失敗,要麼handler里的對象與調用方的對象不是同一個實例,為了解決這個問題,我讓EventBus不再是單例模式,將dicEvent變成了靜態,暫時表面解決

      2、未考慮跨進程的實現(感覺用savorboard大佬的就可以了)

      3、目前這個東西在一個小的新項目里使用,暫時在測試環境還算沒啥問題,各位小夥伴如果有類似需求,可以做個參考

      由於個人原因,在測試上可能會有所不夠,如果有什麼bug的話,還請站內信告知,感謝(ps:文字表達弱雞,技術渣渣,各位多多包涵)

      最後:附上項目地址:

     

     

    作者: Mango

    出處: 

    關於自己:專註.Net桌面開發以及Web後台開發,對.NetCore、微服務、DevOps,K8S等感興趣,最近到了個甲方公司準備休養一段時間

    本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,如有問題, 可站內信告知.

     

     

     

     

     

     

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • java中hashmap容量的初始化

    HashMap使用HashMap(int initialCapacity)對集合進行初始化。

    在默認的情況下,HashMap的容量是16。但是如果用戶通過構造函數指定了一個数字作為容量,那麼Hash會選擇大於該数字的第一個2的冪作為容量。比如如果指定了3,則容量是4;如果指定了7,則容量是8;如果指定了9,則容量是16。

    為什麼要設置HashMap的初始化容量

    在《阿里巴巴Java開發手冊》中,有一條開發建議是建議我們設置HashMap的初始化容量。

    下面我們通過具體的代碼來了解下為什麼會這麼建議。

    我們先來寫一段代碼在JDK1.7的環境下運行,來分別測試下,在不指定初始化容量和指定初始化容量的情況下性能情況的不同。

    public static void main(String[] args) {
        int aHundredMillion = 10000000;
    
        // 未初始化容量
        Map<Integer, Integer> map = new HashMap<>();
        long s1 = System.currentTimeMillis();
        for (int i = 0; i < aHundredMillion; i++) {
            map.put(i, i);
        }
        long s2 = System.currentTimeMillis();
        System.out.println("未初始化容量,耗時: " + (s2 - s1)); // 14322
    
        // 初始化容量為50000000
        Map<Integer, Integer> map1 = new HashMap<>(aHundredMillion / 2);
        long s3 = System.currentTimeMillis();
        for (int i = 0; i < aHundredMillion; i++) {
            map1.put(i, i);
        }
        long s4 = System.currentTimeMillis();
        System.out.println("初始化容量5000000,耗時: " + (s4 - s3)); // 11819
    
        // 初始化容量為100000000
        Map<Integer, Integer> map2 = new HashMap<>(aHundredMillion);
        long s5 = System.currentTimeMillis();
        for (int i = 0; i < aHundredMillion; i++) {
            map2.put(i, i);
        }
        long s6 = System.currentTimeMillis();
        System.out.println("初始化容量為10000000,耗時: " + (s6 - s5)); // 7978
    }

    從以上的代碼不難理解,我們創建了3個HashMap,分別使用默認的容量(16)、使用元素個數的一半(5千萬)作為初始容量和使用元素個數(一億)作為初始容量進行初始化,然後分別向其中put一億個KV。

    從上面的打印結果中可以得到一個初步的結論:在已知HashMap中將要存放的KV個數的時候,設置一個合理的初始化容量可以有效地提高性能。下面我們來簡單分析一下原因。

    我們知道,HashMap是有擴容機制的。所謂的擴容機制,指的是當達到擴容條件的時候,HashMap就會自動進行擴容。而HashMap的擴容條件就是當HashMap中的元素個數(Size)超過臨界值(Threshold)的情況下就會自動擴容。

    threshold = loadFactor * capacity

    在元素個數超過臨界值的情況下,隨着元素的不斷增加,HashMap就會發生擴容,而HashMap中的擴容機制決定了每次擴容都需要重建hash表,這一操作需要消耗大量資源,是非常影響性能的。因此,如果我們沒有設置初始的容量大小,HashMap就可能會不斷髮生擴容,也就使得程序的性能降低了。

    另外,在上面的代碼中我們會發現,同樣是設置了初始化容量,設置的數值不同也會影響性能,那麼當我們已知HashMap中即將存放的KV個數的時候,容量的設置就成了一個問題。

    HashMap中容量的初始化

    開頭提到,在默認的情況下,當我們設置HashMap的初始化容量時,實際上HashMap會採用第一個大於該數值的2的冪作為初始化容量。

    Map<String, String> map = new HashMap<>(1);
    map.put("huangq", "yanggb");
    
    Class<?> mapType = map.getClass();
    Method capacity = mapType.getDeclaredMethod("capacity");
    capacity.setAccessible(true);
    System.out.println("capacity : " + capacity.invoke(map)); // 2

    當初始化的容量設置成1的時候,通過反射取出來的capacity卻是2。在JDK1.8中,如果我們傳入的初始化容量為1,實際上設置的結果也是1。上面的代碼打印的結果為2的原因,是代碼中給map塞入值的操作導致了擴容,容量從1擴容到了2。事實上,在JDK1.7和JDK1.8中,HashMap初始化容量(capacity)的時機不同。在JDK1.8中,調用HashMap的構造函數定義HashMap的時候,就會進行容量的設定。而在JDK1.7中,要等到第一次put操作時才進行這一操作。

    因此,當我們通過HashMap(int initialCapacity)設置初始容量的時候,HashMap並不一定會直接採用我們傳入的數值,而是經過計算,得到一個新值,目的是提高hash的效率。比如1->1、3->4、7->8和9->16。

    HashMap中初始容量的合理值

    通過上面的分析我們可以知道,當我們使用HashMap(int initialCapacity)來初始化容量的時候,JDK會默認幫我們計算一個相對合理的值當做初始容量。那麼,是不是我們只需要把已知的HashMap中即將存放的元素個數直接傳給initialCapacity就可以了呢?

    initialCapacity = (需要存儲的元素個數 / 負載因子) + 1

    這裏的負載因子就是loaderFactor,默認值為0.75。

    initialCapacity = expectedSize / 0.75F + 1.0F

    上面這個公式是《阿里巴巴Java開發手冊》中的一個建議,在Guava中也是提供了相同的算法,更甚之,這個算法實際上是JDK8中putAll()方法的實現。這是公式的得出是因為,當HashMap內部維護的哈希表的容量達到75%時(默認情況下),就會觸發rehash(重建hash表)操作。而rehash的過程是比較耗費時間的。所以初始化容量要設置成expectedSize/0.75 + 1的話,可以有效地減少衝突,也可以減小誤差。

    總結

    當我們想要在代碼中創建一個HashMap的時候,如果我們已知這個Map中即將存放的元素個數,給HashMap設置初始容量可以在一定程度上提升效率。

    但是,JDK並不會直接拿用戶傳進來的数字當做默認容量,而是會進行一番運算,最終得到一個2的冪。而為了最大程度地避免擴容帶來的性能消耗,通常是建議可以把默認容量的数字設置成expectedSize / 0.75F + 1.0F。

    在日常開發中,可以使用Guava提供的一個方法來創建一個HashMap,計算的過程Guava會幫我們完成。

    Map<String, String> map = Maps.newHashMapWithExpectedSize(10);

    最後要說的一點是,這種算法實際上是一種使用內存換取性能的做法,在真正的應用場景中要考慮到內存的影響。

     

    “當你認真喜歡一個人的時候,你的全世界都是她。”

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • [springboot 開發單體web shop] 7. 多種形式提供商品列表

    [springboot 開發單體web shop] 7. 多種形式提供商品列表

    上文回顧

    我們實現了仿jd的輪播廣告以及商品分類的功能,並且講解了不同的注入方式,本節我們將繼續實現我們的電商主業務,商品信息的展示。

    需求分析

    首先,在我們開始本節編碼之前,我們先來分析一下都有哪些地方會對商品進行展示,打開jd首頁,鼠標下拉可以看到如下:

    可以看到,在大類型下查詢了部分商品在首頁進行展示(可以是最新的,也可以是網站推薦等等),然後點擊任何一個分類,可以看到如下:

    我們一般進到電商網站之後,最常用的一個功能就是搜索, 結果如下:

    選擇任意一個商品點擊,都可以進入到詳情頁面,這個是單個商品的信息展示。
    綜上,我們可以知道,要實現一個電商平台的商品展示,最基本的包含:

    • 首頁推薦/最新上架商品
    • 分類查詢商品
    • 關鍵詞搜索商品
    • 商品詳情展示

    接下來,我們就可以開始商品相關的業務開發了。

    首頁商品列表|IndexProductList

    開發梳理

    我們首先來實現在首頁展示的推薦商品列表,來看一下都需要展示哪些信息,以及如何進行展示。

    • 商品主鍵(product_id)
    • 展示圖片(image_url)
    • 商品名稱(product_name)
    • 商品價格(product_price)
    • 分類說明(description)
    • 分類名稱(category_name)
    • 分類主鍵(category_id)
    • 其他…

    編碼實現

    根據一級分類查詢

    遵循開發順序,自下而上,如果基礎mapper解決不了,那麼優先編寫SQL mapper,因為我們需要在同一張表中根據parent_id遞歸的實現數據查詢,當然我們這裏使用的是錶鏈接的方式實現。因此,common mapper無法滿足我們的需求,需要自定義mapper實現。

    Custom Mapper實現

    和根據一級分類查詢子分類一樣,在項目mscx-shop-mapper中添加一個自定義實現接口com.liferunner.custom.ProductCustomMapper,然後在resources\mapper\custom路徑下同步創建xml文件mapper/custom/ProductCustomMapper.xml,此時,因為我們在上節中已經配置了當前文件夾可以被容器掃描到,所以我們添加的新的mapper就會在啟動時被掃描加載,代碼如下:

    /**
     * ProductCustomMapper for : 自定義商品Mapper
     */
    public interface ProductCustomMapper {
    
        /***
         * 根據一級分類查詢商品
         *
         * @param paramMap 傳遞一級分類(map傳遞多參數)
         * @return java.util.List<com.liferunner.dto.IndexProductDTO>
         */
        List<IndexProductDTO> getIndexProductDtoList(@Param("paramMap") Map<String, Integer> paramMap);
    }
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.liferunner.custom.ProductCustomMapper">
        <resultMap id="IndexProductDTO" type="com.liferunner.dto.IndexProductDTO">
            <id column="rootCategoryId" property="rootCategoryId"/>
            <result column="rootCategoryName" property="rootCategoryName"/>
            <result column="slogan" property="slogan"/>
            <result column="categoryImage" property="categoryImage"/>
            <result column="bgColor" property="bgColor"/>
            <collection property="productItemList" ofType="com.liferunner.dto.IndexProductItemDTO">
                <id column="productId" property="productId"/>
                <result column="productName" property="productName"/>
                <result column="productMainImageUrl" property="productMainImageUrl"/>
                <result column="productCreateTime" property="productCreateTime"/>
            </collection>
        </resultMap>
        <select id="getIndexProductDtoList" resultMap="IndexProductDTO" parameterType="Map">
            SELECT
            c.id as rootCategoryId,
            c.name as rootCategoryName,
            c.slogan as slogan,
            c.category_image as categoryImage,
            c.bg_color as bgColor,
            p.id as productId,
            p.product_name as productName,
            pi.url as productMainImageUrl,
            p.created_time as productCreateTime
            FROM category c
            LEFT JOIN products p
            ON c.id = p.root_category_id
            LEFT JOIN products_img pi
            ON p.id = pi.product_id
            WHERE c.type = 1
            AND p.root_category_id = #{paramMap.rootCategoryId}
            AND pi.is_main = 1
            LIMIT 0,10;
        </select>
    </mapper>

    Service實現

    serviceproject 創建com.liferunner.service.IProductService接口以及其實現類com.liferunner.service.impl.ProductServiceImpl,添加查詢方法如下:

    public interface IProductService {
    
        /**
         * 根據一級分類id獲取首頁推薦的商品list
         *
         * @param rootCategoryId 一級分類id
         * @return 商品list
         */
        List<IndexProductDTO> getIndexProductDtoList(Integer rootCategoryId);
        ...
    }
    
    ---
        
    @Slf4j
    @Service
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class ProductServiceImpl implements IProductService {
    
        // RequiredArgsConstructor 構造器注入
        private final ProductCustomMapper productCustomMapper;
    
        @Transactional(propagation = Propagation.SUPPORTS)
        @Override
        public List<IndexProductDTO> getIndexProductDtoList(Integer rootCategoryId) {
            log.info("====== ProductServiceImpl#getIndexProductDtoList(rootCategoryId) : {}=======", rootCategoryId);
            Map<String, Integer> map = new HashMap<>();
            map.put("rootCategoryId", rootCategoryId);
            val indexProductDtoList = this.productCustomMapper.getIndexProductDtoList(map);
            if (CollectionUtils.isEmpty(indexProductDtoList)) {
                log.warn("ProductServiceImpl#getIndexProductDtoList未查詢到任何商品信息");
            }
            log.info("查詢結果:{}", indexProductDtoList);
            return indexProductDtoList;
        }
    }

    Controller實現

    接着,在com.liferunner.api.controller.IndexController中實現對外暴露的查詢接口:

    @RestController
    @RequestMapping("/index")
    @Api(value = "首頁信息controller", tags = "首頁信息接口API")
    @Slf4j
    public class IndexController {
        ...
        @Autowired
        private IProductService productService;
    
        @GetMapping("/rootCategorys")
        @ApiOperation(value = "查詢一級分類", notes = "查詢一級分類")
        public JsonResponse findAllRootCategorys() {
            log.info("============查詢一級分類==============");
            val categoryResponseDTOS = this.categoryService.getAllRootCategorys();
            if (CollectionUtils.isEmpty(categoryResponseDTOS)) {
                log.info("============未查詢到任何分類==============");
                return JsonResponse.ok(Collections.EMPTY_LIST);
            }
            log.info("============一級分類查詢result:{}==============", categoryResponseDTOS);
            return JsonResponse.ok(categoryResponseDTOS);
        }
        ...
    }

    Test API

    編寫完成之後,我們需要對我們的代碼進行測試驗證,還是通過使用RestService插件來實現,當然,大家也可以通過Postman來測試,結果如下:

    商品列表|ProductList

    如開文之初我們看到的京東商品列表一樣,我們先分析一下在商品列表頁面都需要哪些元素信息?

    開發梳理

    商品列表的展示按照我們之前的分析,總共分為2大類:

    • 選擇商品分類之後,展示當前分類下所有商品
    • 輸入搜索關鍵詞后,展示當前搜索到相關的所有商品

    在這兩類中展示的商品列表數據,除了數據來源不同以外,其他元素基本都保持一致,那麼我們是否可以使用統一的接口來根據參數實現隔離呢? 理論上不存在問題,完全可以通過傳參判斷的方式進行數據回傳,但是,在我們實現一些可預見的功能需求時,一定要給自己的開發預留後路,也就是我們常說的可拓展性,基於此,我們會分開實現各自的接口,以便於後期的擴展。
    接着來分析在列表頁中我們需要展示的元素,首先因為需要分上述兩種情況,因此我們需要在我們API設計的時候分別處理,針對於
    1.分類的商品列表展示,需要傳入的參數有:

    • 分類id
    • 排序(在電商列表我們常見的幾種排序(銷量,價格等等))
    • 分頁相關(因為我們不可能把數據庫中所有的商品都取出來)
      • PageNumber(當前第幾頁)
      • PageSize(每頁显示多少條數據)

    2.關鍵詞查詢商品列表,需要傳入的參數有:

    • 關鍵詞
    • 排序(在電商列表我們常見的幾種排序(銷量,價格等等))
    • 分頁相關(因為我們不可能把數據庫中所有的商品都取出來)
      • PageNumber(當前第幾頁)
      • PageSize(每頁显示多少條數據)

    需要在頁面展示的信息有:

    • 商品id(用於跳轉商品詳情使用)
    • 商品名稱
    • 商品價格
    • 商品銷量
    • 商品圖片
    • 商品優惠

    編碼實現

    根據上面我們的分析,接下來開始我們的編碼:

    根據商品分類查詢

    根據我們的分析,肯定不會在一張表中把所有數據獲取全,因此我們需要進行多表聯查,故我們需要在自定義mapper中實現我們的功能查詢.

    ResponseDTO 實現

    根據我們前面分析的前端需要展示的信息,我們來定義一個用於展示這些信息的對象com.liferunner.dto.SearchProductDTO,代碼如下:

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class SearchProductDTO {
        private String productId;
        private String productName;
        private Integer sellCounts;
        private String imgUrl;
        private Integer priceDiscount;
        //商品優惠,我們直接計算之後返回優惠后價格
    }

    Custom Mapper 實現

    com.liferunner.custom.ProductCustomMapper.java中新增一個方法接口:

        List<SearchProductDTO> searchProductListByCategoryId(@Param("paramMap") Map<String, Object> paramMap);

    同時,在mapper/custom/ProductCustomMapper.xml中實現我們的查詢方法:

    <select id="searchProductListByCategoryId" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
            SELECT
            p.id as productId,
            p.product_name as productName,
            p.sell_counts as sellCounts,
            pi.url as imgUrl,
            tp.priceDiscount
            FROM products p
            LEFT JOIN products_img pi
            ON p.id = pi.product_id
            LEFT JOIN
            (
            SELECT product_id, MIN(price_discount) as priceDiscount
            FROM products_spec
            GROUP BY product_id
            ) tp
            ON tp.product_id = p.id
            WHERE pi.is_main = 1
            AND p.category_id = #{paramMap.categoryId}
            ORDER BY
            <choose>
                <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                    p.sell_counts DESC
                </when>
                <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                    tp.priceDiscount ASC
                </when>
                <otherwise>
                    p.created_time DESC
                </otherwise>
            </choose>
        </select>

    主要來說明一下這裏的<choose>模塊,以及為什麼不使用if標籤。
    在有的時候,我們並不希望所有的條件都同時生效,而只是想從多個選項中選擇一個,但是在使用IF標籤時,只要test中的表達式為 true,就會執行IF 標籤中的條件。MyBatis 提供了 choose 元素。IF標籤是與(and)的關係,而 choose 是或(or)的關係。
    它的選擇是按照順序自上而下,一旦有任何一個滿足條件,則選擇退出。

    Service 實現

    然後在servicecom.liferunner.service.IProductService中添加方法接口:

        /**
         * 根據商品分類查詢商品列表
         *
         * @param categoryId 分類id
         * @param sortby     排序方式
         * @param pageNumber 當前頁碼
         * @param pageSize   每頁展示多少條數據
         * @return 通用分頁結果視圖
         */
        CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize);

    在實現類com.liferunner.service.impl.ProductServiceImpl中,實現上述方法:

        // 方法重載
        @Override
        public CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("categoryId", categoryId);
            paramMap.put("sortby", sortby);
            // mybatis-pagehelper
            PageHelper.startPage(pageNumber, pageSize);
            val searchProductDTOS = this.productCustomMapper.searchProductListByCategoryId(paramMap);
            // 獲取mybatis插件中獲取到信息
            PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
            // 封裝為返回到前端分頁組件可識別的視圖
            val commonPagedResult = CommonPagedResult.builder()
                    .pageNumber(pageNumber)
                    .rows(searchProductDTOS)
                    .totalPage(pageInfo.getPages())
                    .records(pageInfo.getTotal())
                    .build();
            return commonPagedResult;
        }

    在這裏,我們使用到了一個mybatis-pagehelper插件,會在下面的福利講解中分解。

    Controller 實現

    繼續在com.liferunner.api.controller.ProductController中添加對外暴露的接口API:

    @GetMapping("/searchByCategoryId")
        @ApiOperation(value = "查詢商品信息列表", notes = "根據商品分類查詢商品列表")
        public JsonResponse searchProductListByCategoryId(
            @ApiParam(name = "categoryId", value = "商品分類id", required = true, example = "0")
            @RequestParam Integer categoryId,
            @ApiParam(name = "sortby", value = "排序方式", required = false)
            @RequestParam String sortby,
            @ApiParam(name = "pageNumber", value = "當前頁碼", required = false, example = "1")
            @RequestParam Integer pageNumber,
            @ApiParam(name = "pageSize", value = "每頁展示記錄數", required = false, example = "10")
            @RequestParam Integer pageSize
        ) {
            if (null == categoryId || categoryId == 0) {
                return JsonResponse.errorMsg("分類id錯誤!");
            }
            if (null == pageNumber || 0 == pageNumber) {
                pageNumber = DEFAULT_PAGE_NUMBER;
            }
            if (null == pageSize || 0 == pageSize) {
                pageSize = DEFAULT_PAGE_SIZE;
            }
            log.info("============根據分類:{} 搜索列表==============", categoryId);
    
            val searchResult = this.productService.searchProductList(categoryId, sortby, pageNumber, pageSize);
            return JsonResponse.ok(searchResult);
        }

    因為我們的請求中,只會要求商品分類id是必填項,其餘的調用方都可以不提供,但是如果不提供的話,我們系統就需要給定一些默認的參數來保證我們的系統正常穩定的運行,因此,我定義了com.liferunner.api.controller.BaseController,用於存儲一些公共的配置信息。

    /**
     * BaseController for : controller 基類
     */
    @Controller
    public class BaseController {
        /**
         * 默認展示第1頁
         */
        public final Integer DEFAULT_PAGE_NUMBER = 1;
        /**
         * 默認每頁展示10條數據
         */
        public final Integer DEFAULT_PAGE_SIZE = 10;
    }

    Test API

    測試的參數分別是:categoryId : 51 ,sortby : price,pageNumber : 1,pageSize : 5

    可以看到,我們查詢到7條數據,總頁數totalPage為2,並且根據價格從小到大進行了排序,證明我們的編碼是正確的。接下來,通過相同的代碼邏輯,我們繼續實現根據搜索關鍵詞進行查詢。

    根據關鍵詞查詢

    Response DTO 實現

    使用上面實現的com.liferunner.dto.SearchProductDTO.

    Custom Mapper 實現

    com.liferunner.custom.ProductCustomMapper中新增方法:

    List<SearchProductDTO> searchProductList(@Param("paramMap") Map<String, Object> paramMap);

    mapper/custom/ProductCustomMapper.xml中添加查詢SQL:

    <select id="searchProductList" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
            SELECT
            p.id as productId,
            p.product_name as productName,
            p.sell_counts as sellCounts,
            pi.url as imgUrl,
            tp.priceDiscount
            FROM products p
            LEFT JOIN products_img pi
            ON p.id = pi.product_id
            LEFT JOIN
            (
            SELECT product_id, MIN(price_discount) as priceDiscount
            FROM products_spec
            GROUP BY product_id
            ) tp
            ON tp.product_id = p.id
            WHERE pi.is_main = 1
            <if test="paramMap.keyword != null and paramMap.keyword != ''">
                AND p.item_name LIKE "%${paramMap.keyword}%"
            </if>
            ORDER BY
            <choose>
                <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                    p.sell_counts DESC
                </when>
                <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                    tp.priceDiscount ASC
                </when>
                <otherwise>
                    p.created_time DESC
                </otherwise>
            </choose>
        </select>

    Service 實現

    com.liferunner.service.IProductService中新增查詢接口:

        /**
         * 查詢商品列表
         *
         * @param keyword    查詢關鍵詞
         * @param sortby     排序方式
         * @param pageNumber 當前頁碼
         * @param pageSize   每頁展示多少條數據
         * @return 通用分頁結果視圖
         */
        CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize);

    com.liferunner.service.impl.ProductServiceImpl實現上述接口方法:

        @Override
        public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("keyword", keyword);
            paramMap.put("sortby", sortby);
            // mybatis-pagehelper
            PageHelper.startPage(pageNumber, pageSize);
            val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
            // 獲取mybatis插件中獲取到信息
            PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
            // 封裝為返回到前端分頁組件可識別的視圖
            val commonPagedResult = CommonPagedResult.builder()
                    .pageNumber(pageNumber)
                    .rows(searchProductDTOS)
                    .totalPage(pageInfo.getPages())
                    .records(pageInfo.getTotal())
                    .build();
            return commonPagedResult;
        }

    上述方法和之前searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize)唯一的區別就是它是肯定搜索關鍵詞來進行數據查詢,使用重載的目的是為了我們後續不同類型的業務擴展而考慮的。

    Controller 實現

    com.liferunner.api.controller.ProductController中添加關鍵詞搜索API:

        @GetMapping("/search")
        @ApiOperation(value = "查詢商品信息列表", notes = "查詢商品信息列表")
        public JsonResponse searchProductList(
            @ApiParam(name = "keyword", value = "搜索關鍵詞", required = true)
            @RequestParam String keyword,
            @ApiParam(name = "sortby", value = "排序方式", required = false)
            @RequestParam String sortby,
            @ApiParam(name = "pageNumber", value = "當前頁碼", required = false, example = "1")
            @RequestParam Integer pageNumber,
            @ApiParam(name = "pageSize", value = "每頁展示記錄數", required = false, example = "10")
            @RequestParam Integer pageSize
        ) {
            if (StringUtils.isBlank(keyword)) {
                return JsonResponse.errorMsg("搜索關鍵詞不能為空!");
            }
            if (null == pageNumber || 0 == pageNumber) {
                pageNumber = DEFAULT_PAGE_NUMBER;
            }
            if (null == pageSize || 0 == pageSize) {
                pageSize = DEFAULT_PAGE_SIZE;
            }
            log.info("============根據關鍵詞:{} 搜索列表==============", keyword);
    
            val searchResult = this.productService.searchProductList(keyword, sortby, pageNumber, pageSize);
            return JsonResponse.ok(searchResult);
        }

    Test API

    測試參數:keyword : 西鳳,sortby : sell,pageNumber : 1,pageSize : 10

    根據銷量排序正常,查詢關鍵詞正常,總條數32,每頁10條,總共3頁正常。

    福利講解

    在本節編碼實現中,我們使用到了一個通用的mybatis分頁插件mybatis-pagehelper,接下來,我們來了解一下這個插件的基本情況。

    mybatis-pagehelper

    如果各位小夥伴使用過:, 那麼對於這個就很容易理解了,它其實就是基於來實現的,當攔截到原始SQL之後,對SQL進行一次改造處理。
    我們來看看我們自己代碼中的實現,根據springboot編碼三部曲:

    1.添加依賴

            <!-- 引入mybatis-pagehelper 插件-->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.2.12</version>
            </dependency>

    有同學就要問了,為什麼引入的這個依賴和我原來使用的不同?以前使用的是:

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.1.10</version>
    </dependency>

    答案就在這裏:

    我們使用的是springboot進行的項目開發,既然使用的是springboot,那我們完全可以用到它的自動裝配特性,作者幫我們實現了這麼一個,我們只需要參考示例來編寫就ok了。

    2.改配置

    # mybatis 分頁組件配置
    pagehelper:
      helperDialect: mysql #插件支持12種數據庫,選擇類型
      supportMethodsArguments: true

    3.改代碼

    如下示例代碼:

        @Override
        public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("keyword", keyword);
            paramMap.put("sortby", sortby);
            // mybatis-pagehelper
            PageHelper.startPage(pageNumber, pageSize);
            val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
            // 獲取mybatis插件中獲取到信息
            PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
            // 封裝為返回到前端分頁組件可識別的視圖
            val commonPagedResult = CommonPagedResult.builder()
                    .pageNumber(pageNumber)
                    .rows(searchProductDTOS)
                    .totalPage(pageInfo.getPages())
                    .records(pageInfo.getTotal())
                    .build();
            return commonPagedResult;
        }

    在我們查詢數據庫之前,我們引入了一句PageHelper.startPage(pageNumber, pageSize);,告訴mybatis我們要對查詢進行分頁處理,這個時候插件會啟動一個攔截器com.github.pagehelper.PageInterceptor,針對所有的query進行攔截,添加自定義參數和添加查詢數據總數。(後續我們會打印sql來證明。)

    當查詢到結果之後,我們需要將我們查詢到的結果通知給插件,也就是PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);com.github.pagehelper.PageInfo是對插件針對分頁做的一個屬性包裝,具體可以查看)。

    至此,我們的插件使用就已經結束了。但是為什麼我們在後面又封裝了一個對象來對外進行返回,而不是使用查詢到的PageInfo呢?這是因為我們實際開發過程中,為了數據結構的一致性做的一次結構封裝,你也可不實現該步驟,都是對結果沒有任何影響的。

    SQL打印對比

    2019-11-21 12:04:21 INFO  ProductController:134 - ============根據關鍵詞:西鳳 搜索列表==============
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4ff449ba] was not registered for synchronization because synchronization is not active
    JDBC Connection [HikariProxyConnection@1980420239 wrapping com.mysql.cj.jdbc.ConnectionImpl@563b22b1] will not be managed by Spring
    ==>  Preparing: SELECT count(0) FROM products p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN (SELECT product_id, MIN(price_discount) AS priceDiscount FROM products_spec GROUP BY product_id) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西鳳%" 
    ==> Parameters: 
    <==    Columns: count(0)
    <==        Row: 32
    <==      Total: 1
    ==>  Preparing: SELECT p.id as productId, p.product_name as productName, p.sell_counts as sellCounts, pi.url as imgUrl, tp.priceDiscount FROM product p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN ( SELECT product_id, MIN(price_discount) as priceDiscount FROM products_spec GROUP BY product_id ) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西鳳%" ORDER BY p.sell_counts DESC LIMIT ? 
    ==> Parameters: 10(Integer)

    我們可以看到,我們的SQL中多了一個SELECT count(0),第二條SQL多了一個LIMIT參數,在代碼中,我們很明確的知道,我們並沒有显示的去搜索總數和查詢條數,可以確定它就是插件幫我們實現的。

    源碼下載

    下節預告

    下一節我們將繼續開發商品詳情展示以及商品評價業務,在過程中使用到的任何開發組件,我都會通過專門的一節來進行介紹的,兄弟們末慌!

    gogogo!

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 面向對象和面向過程詳解

    1.前言

    其實一直對面向過程和面向對象的概念和區別沒有很深入的理解,在自己不斷想完善自己的知識體系中,今天借這個時間,寫一篇博客。來深入的了解面向過程與面向對象!好記性不如爛筆頭!!  

    2.面向對象與面向過程的區別

    面向過程就是分析出解決問題所需要的步驟,然後用函數把這些步驟一步一步實現,使用的時候一個一個依次調用就可以了;面向對象是把構成問題事務分解成各個對象,建立對象的目的不是為了完成一個步驟,而是為了描敘某個事物在整個解決問題的步驟中的行為。

    舉一個下五子棋通俗例子吧 哈哈哈 我感覺這兒例子很容易讓人理解

    面向過程的設計思路就是首先分析問題的步驟:

    1、開始遊戲,

    2、黑子先走,

    3、繪製畫面,

    4、判斷輸贏,

    5、輪到白子,

    6、繪製畫面,

    7、判斷輸贏,

    8、返回步驟2,

    9、輸出最後結果。

    把上面每個步驟用分別的函數來實現,問題就解決了。

    面向對象的設計則是從另外的思路來解決問題。整個五子棋可以分為

    1、黑白雙方,這兩方的行為是一模一樣的,

    2、棋盤系統,負責繪製畫面,

    3、規則系統,負責判定諸如犯規、輸贏等。第一類對象(玩家對象)負責接受用戶輸入,並告知第二類對象(棋盤對象)棋子布局的變化,

    棋盤對象接收到了棋子的i變化就要負責在屏幕上面显示出這種變化,同時利用第三類對象(規則系統)來對棋局進行判定。

    可以明顯地看出,面向對象是以功能來劃分問題,而不是步驟。同樣是繪製棋局,這樣的行為在面向過程的設計中分散在了總多步驟中,很可能出現不同的繪製版本,因為通常設計人員會考慮到實際情況進行各種各樣的簡化。而面向對象的設計中,繪圖只可能在棋盤對象中出現,從而保證了繪圖的統一。

    功能上的統一保證了面向對象設計的可擴展性。比如要加入悔棋的功能,如果要改動面向過程的設計,那麼從輸入到判斷到显示這一連串的步驟都要改動,甚至步驟之間的循序都要進行大規模調整。如果是面向對象的話,只用改動棋盤對象就行了,棋盤系統保存了黑白雙方的棋譜,簡單回溯就可以了,而显示和規則判斷則不用顧及,同時整個對對象功能的調用順序都沒有變化,改動只是局部的。

    再比如我要把這個五子棋遊戲改為圍棋遊戲,如果你是面向過程設計,那麼五子棋的規則就分佈在了你的程序的每一個角落,要改動還不如重寫。但是如果你當初就是面向對象的設計,那麼你只用改動規則對象就可以了,五子棋和圍棋的區別不就是規則嗎?(當然棋盤大小好像也不一樣,但是你會覺得這是一個難題嗎?直接在棋盤對象中進行一番小改動就可以了。)而下棋的大致步驟從面向對象的角度來看沒有任何變化。

    當然,要達到改動只是局部的需要設計的人有足夠的經驗,使用對象不能保證你的程序就是面向對象,初學者或者很蹩腳的程序員很可能以面向對象之虛而行面向過程之實,這樣設計出來的所謂面向對象的程序很難有良好的可移植性和可擴展性

    三、面向過程與面向對象的優缺點
    很多資料上全都是一群很難理解的理論知識,整的我頭都大了,後來發現了一個比較好的文章,寫的真是太棒了,通俗易懂,想要不明白都難!

    用面向過程的方法寫出來的程序是一份蛋炒飯,而用面向對象寫出來的程序是一份蓋澆飯。所謂蓋澆飯,北京叫蓋飯,東北叫燴飯,廣東叫碟頭飯,就是在一碗白米飯上面澆上一份蓋菜,你喜歡什麼菜,你就澆上什麼菜。我覺得這個比喻還是比較貼切的。

    蛋炒飯製作是把米飯和雞蛋混在一起炒勻。蓋澆飯呢,則是把米飯和蓋菜分別做好,你如果要一份紅燒肉蓋飯呢,就給你澆一份紅燒肉;如果要一份青椒土豆蓋澆飯,就給澆一份青椒土豆絲。

    蛋炒飯的好處就是入味均勻,吃起來香。如果恰巧你不愛吃雞蛋,只愛吃青菜的話,那麼唯一的辦法就是全部倒掉,重新做一份青菜炒飯了。蓋澆飯就沒這麼多麻煩,你只需要把上面的蓋菜撥掉,更換一份蓋菜就可以了。蓋澆飯的缺點是入味不均,可能沒有蛋炒飯那麼香。

    到底是蛋炒飯好還是蓋澆飯好呢?其實這類問題都很難回答,非要比個上下高低的話,就必須設定一個場景,否則只能說是各有所長。如果大家都不是美食家,沒那麼多講究,那麼從飯館角度來講的話,做蓋澆飯顯然比蛋炒飯更有優勢,他可以組合出來任意多的組合,而且不會浪費。

    蓋澆飯的好處就是”菜”“飯”分離,從而提高了製作蓋澆飯的靈活性。飯不滿意就換飯,菜不滿意換菜。用軟件工程的專業術語就是”可維護性“比較好,”飯” 和”菜”的耦合度比較低。蛋炒飯將”蛋”“飯”攪和在一起,想換”蛋”“飯”中任何一種都很困難,耦合度很高,以至於”可維護性”比較差。軟件工程追求的目標之一就是可維護性,可維護性主要表現在3個方面:可理解性、可測試性和可修改性。面向對象的好處之一就是顯著的改善了軟件系統的可維護性。
    看了這篇文章,簡單的總結一下!

    面向過程

    優點:性能比面向對象高,因為類調用時需要實例化,開銷比較大,比較消耗資源;比如嵌入式開發、 Linux/Unix等一般採用面向過程開發,性能是最重要的因素。
    缺點:沒有面向對象易維護、易復用、易擴展
    面向對象

    優點:易維護、易復用、易擴展,由於面向對象有封裝、繼承、多態性的特性,可以設計出低耦合的系統,使系統 更加靈活、更加易於維護

    缺點:性能比面向過程低

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • NetCore3.0 文件上傳與大文件上傳的限制

    NetCore文件上傳兩種方式

      NetCore官方給出的兩種文件上傳方式分別為“緩衝”、“流式”。我簡單的說說兩種的區別,

      1.緩衝:通過模型綁定先把整個文件保存到內存,然後我們通過IFormFile得到stream,優點是效率高,缺點對內存要求大。文件不宜過大。

      2.流式處理:直接讀取請求體裝載后的Section 對應的stream 直接操作strem即可。無需把整個請求體讀入內存,

    以下為官方微軟說法

    緩衝

      整個文件讀入 IFormFile,它是文件的 C# 表示形式,用於處理或保存文件。 文件上傳所用的資源(磁盤、內存)取決於併發文件上傳的數量和大小。 如果應用嘗試緩衝過多上傳,站點就會在內存或磁盤空間不足時崩潰。 如果文件上傳的大小或頻率會消耗應用資源,請使用流式傳輸。

    流式處理   

      從多部分請求收到文件,然後應用直接處理或保存它。 流式傳輸無法顯著提高性能。 流式傳輸可降低上傳文件時對內存或磁盤空間的需求。

    文件大小限制

      說起大小限制,我們得從兩方面入手,1應用服務器Kestrel 2.應用程序(我們的netcore程序),

    1.應用服務器Kestre設置

      應用服務器Kestrel對我們的限制主要是對整個請求體大小的限制通過如下配置可以進行設置(Program -> CreateHostBuilder),超出設置範圍會報 BadHttpRequestException: Request body too large 異常信息

    public static IHostBuilder CreateHostBuilder(string[] args) =>
               Host.CreateDefaultBuilder(args)
                   .ConfigureWebHostDefaults(webBuilder =>
                   {
                       webBuilder.ConfigureKestrel((context, options) =>
                       {
                           //設置應用服務器Kestrel請求體最大為50MB
                           options.Limits.MaxRequestBodySize = 52428800;
                       });
                       webBuilder.UseStartup<Startup>();
    });

    2.應用程序設置

      應用程序設置 (Startup->  ConfigureServices) 超出設置範圍會報InvalidDataException 異常信息

    services.Configure<FormOptions>(options =>
     {
                 options.MultipartBodyLengthLimit = long.MaxValue;
     });

    通過設置即重置文件上傳的大小限制。

    源碼分析

      這裏我主要說一下 MultipartBodyLengthLimit  這個參數他主要限制我們使用“緩衝”形式上傳文件時每個的長度。為什麼說是緩衝形式中,是因為我們緩衝形式在讀取上傳文件用的幫助類為 MultipartReaderStream 類下的 Read 方法,此方法在每讀取一次後會更新下讀入的總byte數量,當超過此數量時會拋出  throw new InvalidDataException($Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.);  主要體現在 UpdatePosition 方法對 _observedLength  的判斷

    以下為 MultipartReaderStream 類兩個方法的源代碼,為方便閱讀,我已精簡掉部分代碼

    Read

    public override int Read(byte[] buffer, int offset, int count)
     {
              
              var bufferedData = _innerStream.BufferedData;
          int read;
          read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count));
              return UpdatePosition(read);
    }

    UpdatePosition

    private int UpdatePosition(int read)
            {
                _position += read;
                if (_observedLength < _position)
                {
                    _observedLength = _position;
                    if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault())
                    {
                        throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.");
                    }
                }
                return read;
    }

    通過代碼我們可以看到 當你做了 MultipartBodyLengthLimit 的限制后,在每次讀取後會累計讀取的總量,當讀取總量超出

     MultipartBodyLengthLimit  設定值會拋出 InvalidDataException 異常,

    最終我的文件上傳Controller如下

      需要注意的是我們創建 MultipartReader 時並未設置 BodyLengthLimit  (這參數會傳給 MultipartReaderStream.LengthLimit )也就是我們最終的限制,這裏我未設置值也就無限制,可以通過 UpdatePosition 方法體現出來

    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.WebUtilities;
    using Microsoft.Net.Http.Headers;
    using System.IO;
    using System.Threading.Tasks;
     
    namespace BigFilesUpload.Controllers
    {
        [Route("api/[controller]")]
        public class FileController : Controller
        {
            private readonly string _targetFilePath = "C:\\files\\TempDir";
     
            /// <summary>
            /// 流式文件上傳
            /// </summary>
            /// <returns></returns>
            [HttpPost("UploadingStream")]
            public async Task<IActionResult> UploadingStream()
            {
     
                //獲取boundary
                var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value;
                //得到reader
                var reader = new MultipartReader(boundary, HttpContext.Request.Body);
                //{ BodyLengthLimit = 2000 };//
                var section = await reader.ReadNextSectionAsync();
     
                //讀取section
                while (section != null)
                {
                    var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
                    if (hasContentDispositionHeader)
                    {
                        var trustedFileNameForFileStorage = Path.GetRandomFileName();
                        await WriteFileAsync(section.Body, Path.Combine(_targetFilePath, trustedFileNameForFileStorage));
                    }
                    section = await reader.ReadNextSectionAsync();
                }
                return Created(nameof(FileController), null);
            }
     
            /// <summary>
            /// 緩存式文件上傳
            /// </summary>
            /// <param name=""></param>
            /// <returns></returns>
            [HttpPost("UploadingFormFile")]
            public async Task<IActionResult> UploadingFormFile(IFormFile file)
            {
                using (var stream = file.OpenReadStream())
                {
                    var trustedFileNameForFileStorage = Path.GetRandomFileName();
                    await WriteFileAsync(stream, Path.Combine(_targetFilePath, trustedFileNameForFileStorage));
                }
                return Created(nameof(FileController), null);
            }
     
     
            /// <summary>
            /// 寫文件導到磁盤
            /// </summary>
            /// <param name="stream"></param>
            /// <param name="path">文件保存路徑</param>
            /// <returns></returns>
            public static async Task<int> WriteFileAsync(System.IO.Stream stream, string path)
            {
                const int FILE_WRITE_SIZE = 84975;//寫出緩衝區大小
                int writeCount = 0;
                using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write, FILE_WRITE_SIZE, true))
                {
                    byte[] byteArr = new byte[FILE_WRITE_SIZE];
                    int readCount = 0;
                    while ((readCount = await stream.ReadAsync(byteArr, 0, byteArr.Length)) > 0)
                    {
                        await fileStream.WriteAsync(byteArr, 0, readCount);
                        writeCount += readCount;
                    }
                }
                return writeCount;
            }
     
        }
    }

     

     總結:

    如果你部署 在iis上或者Nginx 等其他應用服務器 也是需要注意的事情,因為他們本身也有對請求體的限制,還有值得注意的就是我們在創建文件流對象時 緩衝區的大小盡量不要超過netcore大對象的限制。這樣在併發高的時候很容易觸發二代GC的回收.

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 傳蘋果電動汽車將採用韓國公司的電池技術

    傳蘋果電動汽車將採用韓國公司的電池技術

    根據行業消息,蘋果近期與一家韓國電池開發商簽署了保密協議,聯合為代號為“泰坦”的汽車專案開發電池。從今年初開始,他們一直在韓國做行政工作。一名蘋果員工一直在這家韓國公司進行參觀活動,他屬於與蘋果電動汽車電池開發相關的部門。  
      業界認為,這家韓國公司並不是唯一一家負責蘋果電池開發的公司。不過,有消息稱,儘管蘋果從一開始就從完全不同的設計、功能以及性能角度來開發電池,但是他們仍舊一直在挖掘創新技術。業界相信,蘋果專注于開發出只能存在於蘋果自動駕駛汽車的創新電池技術。   這家韓國電池開發商由大約20名電池專家組成,持有空芯電池的國際專利技術。這些電池是圓柱形鋰離子二次電池,有兩根手指那麼厚,不同於其它空芯電池。蘋果並未選擇當前電動汽車普遍使用的標準圓形或矩形電池,但計畫根據韓國公司的空芯電池技術為其電動汽車開發自主電池。   文章來源:鳳凰科技

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!