ORM 是一種討厭的反模式
本文由碼農網 – 孫騰浩原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃!
TL;DR(“Too Long; Didn’t Read.”太長不想看,可以看這段摘要 )ORM是一種討厭的反模式,違背了所有物件導向的原則。將物件分隔開並放入被動的資料包中。ORM沒有理由存在任何應用中,無論是小的網路應用還是涉及增刪改查上千張表的企業系統。用什麼來替代它呢?SQL物件(SQL-speaking objects)。
ORM如何工作
Object-relational mapping(ORM)是一種連結關係型資料庫和麵向物件語言(比如Java)的技術。在各種語言中有大量ORM的實現;比如:Java中的Hibernate,Ruby on Rails中的ActiveRecord,PHP中的Doctrine,Python中的SQLAlchemy。在Java中,甚至把ORM設計作為JPA標準。
首先,讓我們看看ORM如何工作。比如,我們用Java,PostgreSQL和Hibernate。我們在資料庫有張表,叫 post:
id | date | title |
---|---|---|
9 | 10/24/2014 | How to cook a sandwich |
13 | 11/03/2014 | My favorite movies |
27 | 11/17/2014 | How much I love my job |
如果我們在Java應用中對這張表增刪改查(CRUD:create, read, update, and delete).首先,我們建立一個 Post 類(很抱歉程式碼很長,我儘量簡潔一點)
@Entity @Table(name = "post") public class Post { private int id; private Date date; private String title; @Id @GeneratedValue public int getId() { return this.id; } @Temporal(TemporalType.TIMESTAMP) public Date getDate() { return this.date; } public Title getTitle() { return this.title; } public void setDate(Date when) { this.date = when; } public void setTitle(String txt) { this.title = txt; } }
在Hibernate做任何操作前,我們要建立一個session工廠:
SessionFactory factory = new AnnotationConfiguration() .configure() .addAnnotatedClass(Post.class) .buildSessionFactory();
這個工廠能在每次我們需要操作 Post物件時給我們”session”。任何有關session的操作應該包裹下面程式碼塊:
Session session = factory.openSession(); try { Transaction txn = session.beginTransaction(); // your manipulations with the ORM, see below txn.commit(); } catch (HibernateException ex) { txn.rollback(); } finally { session.close(); }
當session準備就緒,下面是我們從資料庫表中獲取所有 post:
List posts = session.createQuery("FROM Post").list(); for (Post post : (List<Post>) posts){ System.out.println("Title: " + post.getTitle()); }
我認為到這就很簡單了。Hibernate是一個強大的引擎,連結資料庫,執行SQL SELECT請求,然後取得資料。然後例項化 Post物件並裝填資料。當我們得到物件時,它已經有了資料,比如我們上面使用 getTitle()。
當我們想進行反向操作,將一個物件存入資料庫,操作相同,順序相反。我們先例項化 Post物件,裝填資料,然後讓Hibernate儲存它:
Post post = new Post(); post.setDate(new Date()); post.setTitle("How to cook an omelette"); session.save(post);
這就是所有的ORM如何工作。基本原則總是一樣的——ORM物件就是資料的信使。我們與ORM框架互動。框架與資料庫互動。物件只是幫助我們向框架傳送請求並處理響應。除了get和set,沒有其他方法,我們甚至不知道資料庫在哪。
這就是物件-關係對映如何工作。
你或許會問哪裡有問題?到處!
ORM有什麼問題?
講真的,這有什麼問題?Hibernate成為最受歡迎的Java庫已有10多年,幾乎所有處理SQL的應用都在使用它,每個Java教程都會介紹Hibernate(或其他ORM,比如TopLink和OpenJPA)作為資料庫連線應用,它已成為一個標準。我還要認為它有問題嗎?當然。
我想說整個ORM的構想就是有問題的,它的發明簡直是物件導向程式設計裡NULL指標之後的第二大錯誤。
事實上,我並不是第一個指出這個問題的人,有眾多知名作者討論這個話題,包括Martin Fowler寫的OrmHate(雖然不是反對ORM,但是也值得關注),Jeff Atwood寫的 Object-Relational Mapping Is the Vietnam of Computer Science,Ted Neward寫的The Vietnam of Computer Science,Laurie Voss寫的ORM Is an Anti-Pattern等等。
然而,我的論點不同於上面幾位,儘管他們的論點實用又有根據,比如“ORM很慢”或“資料庫升級困難”,他們沒抓住重點。你可以在Bozhidar Bozhanov的ORM Haters Don’t Get It這篇部落格中找到很棒的論點。
重點是ORM沒有在物件中封裝資料庫的互動,而是把固定的資料和活動的互動分隔開。一部分在物件中儲存資料,另一部分由ORM引擎實現(session工廠)來與資料庫互動資料。上面這張圖展示了ORM做了什麼。
我作為 post資料的操作者,需要處理兩個元件:一是ORM,二是返回的”刪減版”(指只有get和set方法)物件。物件導向程式設計強調的是關注單一的切入點,也就是物件。但在ORM中,我需要關注兩個切入點——ORM引擎和我們甚至不能稱之為物件的”東西”。
因為這嚴重違背了物件導向的正規化,我們可以從很多德高望重的論文中找到實用的解決方案,我可以再提供更多的解決方案。
SQL不再隱藏(SQL Is Not Hidden),ORM使用者常使用SQL(或其他方言,比如HQL)。上面例子中,我們呼叫 session.createQuery(“FROM Post”) 來獲取所有 post。即使這不是SQL,但很像SQL,所以關係模型並沒有封裝在物件中。反而暴露在整個應用中,每個人操作物件時無可避免的需要處理關係模型,來獲取或儲存資料。所以ORM並沒有隱藏和包裹SQL,反而使其暴露在整個應用中。
難於測試。如果某個物件處理post的陣列,它必須處理 SessionFactory的例項。我們如何mock這個依賴呢?看上面的程式碼,你就會意識到單元測試會有多繁瑣和麻煩。相反,我們可以編寫整合測試,用整個應用連結到虛構的測試PostgreSQL。這樣,我們就不需要mock一個 SessionFactory,但這樣的測試會很慢,更重要的是,我們虛構的和資料庫無關這個物件會和資料庫例項衝突。這是個可怕的設計。
我再次重申,ORM的實際問題就是這種後果,本質缺點就是ORM將物件分隔開,嚴重違反了物件的含義。
SQL物件(SQL-Speaking Objects)
有什麼解決方案?讓我給你舉個例子,我們來設計一個 Post類,我們需要將它分成兩個類:Post和 Posts,單個和多個。我曾在我的一篇文章中提到過,一個好的物件是現實生活中實體的抽象,我們來實踐這一原則。我們有兩個實體:資料庫表和表格行,這就是為什麼我們建立兩個類:Post展示表,Post展示行。
我也曾在文章中說過,每個物件應該關聯並實現一個介面,讓我們先來建立兩個介面,當然我們的物件是不可變的,Posts應該是這樣的:
interface Posts { Iterable<Post> iterate(); Post add(Date date, String title); }
單一的 Post應該是這樣的:
interface Post { int id(); Date date(); String title(); }
遍歷資料庫中的所有的post:
Posts posts = // we'll discuss this right now for (Post post : posts.iterate()){ System.out.println("Title: " + post.title()); }
建立一個新post:
Posts posts = // we'll discuss this right now posts.add(new Date(), "How to cook an omelette");
你可以看到,我們有真實物件了,他們掌握所有操作,並且在內部隱藏實現細節,沒有事務,會話或工廠,我們甚至不知道這些物件是否真的和PostgreSQL互動,或許它只是把資料儲存在txt檔案中。Posts帶給我們的是獲取post列表和建立新post的能力,具體實現很好地隱藏在其中,現在讓我們看一看如何實現這兩個類。
我將使用jcabi-jdbc作為JDBC包裹,當然你也可以使用其他你喜歡的JDBC,這無所謂,重點是與資料庫的互動要隱藏在物件中,讓我們開始實現 PgPosts類(“pg”表示PostgreSQL)。
final class PgPosts implements Posts { private final Source dbase; public PgPosts(DataSource data) { this.dbase = data; } public Iterable<Post> iterate() { return new JdbcSession(this.dbase) .sql("SELECT id FROM post") .select( new ListOutcome<Post>( new ListOutcome.Mapping<Post>() { @Override public Post map(final ResultSet rset) { return new PgPost( this.dbase, rset.getInteger(1) ); } } ) ); } public Post add(Date date, String title) { return new PgPost( this.dbase, new JdbcSession(this.dbase) .sql("INSERT INTO post (date, title) VALUES (?, ?)") .set(new Utc(date)) .set(title) .insert(new SingleOutcome<Integer>(Integer.class)) ); } }
然後我們建立 PgPost類來實現 Post介面:
final class PgPost implements Post { private final Source dbase; private final int number; public PgPost(DataSource data, int id) { this.dbase = data; this.number = id; } public int id() { return this.number; } public Date date() { return new JdbcSession(this.dbase) .sql("SELECT date FROM post WHERE id = ?") .set(this.number) .select(new SingleOutcome<Utc>(Utc.class)); } public String title() { return new JdbcSession(this.dbase) .sql("SELECT title FROM post WHERE id = ?") .set(this.number) .select(new SingleOutcome<String>(String.class)); } }
下面就是我們用剛剛建立的類來和資料庫互動:
Posts posts = new PgPosts(dbase); for (Post post : posts.iterate()){ System.out.println("Title: " + post.title()); } Post post = posts.add(new Date(), "How to cook an omelette"); System.out.println("Just added post #" + post.id());
你可以在這看到完整例子.這是一個開源的web app使用PostgreSQL來實現上面提到的-SQL-speaking objects.
效能如何?
我能聽到你的驚呼“效能怎麼樣?”在上面幾行程式碼中,我們建立了和資料庫的冗餘連結。首先我們用 SELECT id來檢索post的ID,然後為了獲取它們的title,我們對應每個post傳送 SELECT title請求,這確實效率很低。
不要擔心,這是物件導向程式設計,意味著這是可伸縮的!我們來建立一個 PgPost的裝飾器,其建構函式接受資料,並在內部快取:
final class ConstPost implements Post { private final Post origin; private final Date dte; private final String ttl; public ConstPost(Post post, Date date, String title) { this.origin = post; this.dte = date; this.ttl = title; } public int id() { return this.origin.id(); } public Date date() { return this.dte; } public String title() { return this.ttl; } }
注意:這個裝飾器並不知道PostgreSQL或JDBC,它僅僅是 POST物件的裝飾,並快取資料和title。通常,這個裝飾器是不可變的。
現在我們來建立 Posts的另一個實現,其返回一個”不可變”的物件:
final class ConstPgPosts implements Posts { // ... public Iterable<Post> iterate() { return new JdbcSession(this.dbase) .sql("SELECT * FROM post") .select( new ListOutcome<Post>( new ListOutcome.Mapping<Post>() { @Override public Post map(final ResultSet rset) { return new ConstPost( new PgPost( ConstPgPosts.this.dbase, rset.getInteger(1) ), Utc.getTimestamp(rset, 2), rset.getString(3) ); } } ) ); } }
現在所有post通過這個 iterate()方法返回,並且從資料庫中取到了資料裝配在新的類中。
使用裝飾器和對相同介面的眾多實現,你可以組合任意的功能,最重要的是擴充套件功能的同時,不要增加設計的複雜度,因為類的大小不會增長,我們使用新的高聚合的類,它們更小巧。
事務又如何?
每個物件應該在其單獨的事務中執行,並且將 SELECT和 INSERT封裝在一起,這需要內建事務,資料庫提供非常棒的支援。如果沒有這樣的支援,建立一個會話事務物件必須接受一個”callable”類,比如:
final class Txn { private final DataSource dbase; public <T> T call(Callable<T> callable) { JdbcSession session = new JdbcSession(this.dbase); try { session.sql("START TRANSACTION").exec(); T result = callable.call(); session.sql("COMMIT").exec(); return result; } catch (Exception ex) { session.sql("ROLLBACK").exec(); throw ex; } } }
然後,當你想在一個事務中進行一系列的操作,像下面這樣:
new Txn(dbase).call( new Callable<Integer>() { @Override public Integer call() { Posts posts = new PgPosts(dbase); Post post = posts.add(new Date(), "How to cook an omelette"); posts.comments().post("This is my first comment!"); return post.id(); } } );
這段程式碼會建立一個新的post並提交一個comment.如果任何呼叫失敗,整個事務將回滾。
對於我來說,這就是物件導向,我稱它為”SQL-speaking objects”,因為它們知道如何與資料庫伺服器通過SQL互動,這是它們的能力,完美封裝在其內部。
譯文連結:http://www.codeceo.com/article/orm-is-offensive-anti-pattern.html
英文原文:ORM Is an Offensive Anti-Pattern
翻譯作者:碼農網 – 孫騰浩
[ 轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]
相關文章
- ORM是明顯的反模式ORM模式
- 實體服務是一種反模式模式
- Java三種面試者是面試官最討厭的,見之即斃!Java面試
- [譯] JavaScript 的函數語言程式設計是一種反模式JavaScript函數程式設計模式
- 去掉那討厭的windows域Windows
- Spotify模型的統一運維開發者門戶Backstage是一種反模式 - lastweekinaws模型運維模式AST
- 《被討厭的勇氣》總結
- 我討厭技術猿
- OpenSessionInView是反模式SessionView模式
- 越來越討厭爬蟲爬蟲
- 7種微服務反模式微服務模式
- 我討厭智力題,我還是個程式設計師嗎?程式設計師
- Stack Overflow:最令人討厭的程式語言
- 令人討厭的程式語言排行榜
- [譯] 熱愛 JavaScript,但是討厭 CSS ?JavaScriptCSS
- Knative是FaaS的反模式嗎?模式
- 程式猿討厭沒有價值的任務
- 關閉ubuntu討厭的內部錯誤提示Ubuntu
- 最喜歡與最討厭的程式語言
- [譯] 通知是一種「暗模式」嗎?模式
- 你的軟體招人討厭的4大原因
- 為什麼Event Sourcing是一種微服務通訊反模式 - Oliver Libutzki微服務模式
- 我不討厭JS,只是更愛CSSJSCSS
- 你知道嗎,Java之父也討厭BugJava
- 為什麼我如此討厭scrums? - RedditScrum
- 我為什麼討厭GNU/Linux?Linux
- Go 語言中常見的幾種反模式Go模式
- 如何做一個優秀的專案經理|你最討厭的寫文件其實是最重要的
- Optional.isPresent()是反模式的用法 - stephan模式
- 開發者最討厭的程式語言:PHP、Ruby 中槍PHP
- 使用者討厭你的App的8大原因APP
- 程式語言簡史:有人討厭花括號,於是發明了PythonPython
- 系列:開源是一種開發模式、商業模式還是其他什麼?(一)模式
- Java的Void方法是反模式的? - DZoneJava模式
- 3個每個人都討厭的Java實踐 - MilošJava
- 程式設計師討厭沒有價值的任務程式設計師
- 不做讓開發人員討厭的產品經理
- RAVE:Twitter使用者最討厭的遊戲公司 育碧第一卡普空第二遊戲