周末花了一個(gè)下午和一個(gè)晚上把Scott Rosenberg的Dreaming in Code從頭到尾看了一遍。最直接的感受是這本書把我之前很多記憶碎片串在了一起,成為一個(gè)生動(dòng)而"完整"的故事,雖然書的內(nèi)容本身組織的多少還是有點(diǎn)散。
我本人對(duì)Chandler這個(gè)項(xiàng)目并不陌生:之前出于對(duì)Python/wxWidget和開源本身的興趣,陸續(xù)用過幾個(gè)0.x的版本,一開始并不是十分友好,性能上也有問題,甚至?xí)某缘粑覚C(jī)器上的數(shù)百兆(或者上G?)空間。后來的版本在性能和可用性上確實(shí)提高了不少,但一直感覺這個(gè)項(xiàng)目缺少必要的、以及許多開源項(xiàng)目應(yīng)有的momentum。Phillip J. Eby對(duì)Chandler開發(fā)人員不懂Python的批評(píng),當(dāng)時(shí)我的印象也很深。而項(xiàng)目中出現(xiàn)的人物,包括Mitchell Kapor、Ted Leung,也都在Chandler這個(gè)范疇之外follow過。其他細(xì)節(jié)包括:Chandler和Cosmo這兩個(gè)名稱的由來、Chandler項(xiàng)目組中女性成員相對(duì)高的比例、一些熟悉的人物及其觀點(diǎn)(Alan Kay, Bill Joy, Frederick Brooks, Donald Knuth、Linus Torvalds, Ward Cunningham, Larry Wall, Guido van Rossum, Joel Spolsky, etc.)、一些公司的分分和和以及人員流動(dòng)等等。感覺挺親切的。
可能更重要、也更深刻的原因是:盡管書中一再提到"There's no such thing as a typical software project, every project is different",我仍然深深的感覺到,Chandler遇到的這些問題,其實(shí)也是我親歷的很多項(xiàng)目的種種問題的一個(gè)縮影。對(duì)這些問題,作者并沒有給出解決方案,其實(shí)誰(shuí)也沒有標(biāo)準(zhǔn)答案。軟件開發(fā)是一項(xiàng)非常具有挑戰(zhàn)性的工作,也正是像我們這樣有熱情、有涵養(yǎng)的專業(yè)人士生存的空間和價(jià)值所在。
以下是一段視頻,Ward Cunningham針對(duì)Debt Metaphor這個(gè)隱喻的由來和人們對(duì)它的一些誤解進(jìn)行了澄清:
我最感興趣的是Burden這一段:Cunningham解釋說,經(jīng)常看到有些開發(fā)團(tuán)隊(duì),他們快速的開發(fā)出軟件產(chǎn)品推向市場(chǎng),不斷的往里面添加新的特性,在這個(gè)過程中,不斷的學(xué)習(xí),但從不把學(xué)到的東西、總結(jié)的經(jīng)驗(yàn)教訓(xùn)應(yīng)用回去,這就像是從銀行借貸,但從不想著有一天需要償還(是不是有點(diǎn)像是在說引發(fā)這次次貸危機(jī)的美國(guó)人的消費(fèi)習(xí)慣和觀念?),到最后,你所有的收入都將用于償還利息,你的購(gòu)買力也將降為零。映射回軟件開發(fā)的語(yǔ)境,如果我們?cè)谝粋€(gè)較長(zhǎng)的時(shí)間跨度內(nèi),開發(fā)一個(gè)軟件,不斷的增加feature,但從不回過頭去整理和重構(gòu),把對(duì)這個(gè)軟件和這些特性的新的理解和認(rèn)知寫回去,那么最終這個(gè)軟件、這些feature將不再會(huì)有任何實(shí)際的價(jià)值,對(duì)它們做任何事,都將花費(fèi)越來越多的時(shí)間和精力,開發(fā)進(jìn)度也就因此下降為零。
經(jīng)歷了有史以來最忙碌的一周,當(dāng)然要好好放松一下,除了聽上乘的古典音樂,沏上一壺上等的烏龍細(xì)細(xì)品味,也是一種享受。烏龍茶和紫砂壺可是絕配,如果是安溪的鐵觀音,加上做工精良的宜興紫砂壺,那滋味,唇齒留香,別提多愜意了。
好的紫砂壺是需要"養(yǎng)"的,今天專程去茶城敗了一只回來,開始"喂"鐵觀音,哈哈。
先簡(jiǎn)單介紹一下問題的語(yǔ)境。
手頭有個(gè)開發(fā)了3年的Spring+iBATIS應(yīng)用(經(jīng)典三層架構(gòu)),最近嘗試用Hibernate實(shí)現(xiàn)原有SQLMap版的部分CRUD操作。除開混合事務(wù)和其他一些底層配置細(xì)節(jié)(如TransactionAwareDataSource、禁用lazy-load等)之外,最大的一個(gè)"pattern-mismatch"是:Model層和持久層采用了dirty flag機(jī)制,即每次INSERT和UPDATE操作,都會(huì)根據(jù)每個(gè)字段的dirty與否決定是否參加INSERT/UPDATE,而這些dirty flag可以被外部重置,所以業(yè)務(wù)層的代碼,經(jīng)常可以看到類似這樣的pattern:
1- new一個(gè)model類并setId() (或者在已有的model上重置dirty flag)
2- set需要update的字段(通常都只是部分字段)
3- 丟給DAO層去update
最終的效果是某張表某個(gè)ID的某條記錄的某些字段被更新了,而其他字段不受影響,這就是我在標(biāo)題中提到的所謂"暴力"update,雖不elegant,但卻也簡(jiǎn)單實(shí)用,至少很"直接"。
為了讓Hibernate版的DAO(默認(rèn)除Trasient之外全體字段參加INSERT和UPDATE)繼續(xù)支持這樣的"use-pattern",除了按照Hibernate的習(xí)慣去配置映射和SessionFactory等之外,我們需要做一些額外的工作:
1- 在BO/Entity類上追加注解
@org.hibernate.annotations.Entity(dynamicInsert = true ,?dynamicUpdate = true )
2- 實(shí)現(xiàn)org.hibernate.Interceptor接口的findDirty()方法,Hibernate提供了一個(gè)EmptyInterceptor可以作為起點(diǎn),方法簽名如下:
public ? int []?findDirty( ????Object?entity,? ????Serializable?id,? ????Object[]?currentState,? ????Object[]?previousState,? ????String[]?propertyNames,? ????Type[]?types );
返回的int[]包含所有應(yīng)該被認(rèn)為是dirty的字段索引,返回null表示默認(rèn)處理,傳入的entity是持久對(duì)象,字段列表會(huì)通過propertyNames參數(shù)傳給你。
3- 注入到Spring的Application Context中,類似這樣:
< bean? id ="findDirtyInterceptor" ?class ="gao.sean.hybrid.interceptor.FindDirtyInterceptor" /> < bean? id ="sessionFactory" ????class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" > ???? ????< property? name ="entityInterceptor" >< ref? bean ="findDirtyInterceptor" /></ property > ????</ bean >
在這樣的配置下,業(yè)務(wù)層的代碼就可以繼續(xù)"暴力"update了。
如果你使用早前版本的Spring,又恰好采用了Annotation注解方式(而非傳統(tǒng)XML方式)配置Hibernate對(duì)象關(guān)系映射,那么在通過org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean配置sessionFactory時(shí),你一定對(duì)annotatedClasses、annotatedPackages有一種說不出的胸悶的感覺,如此以高配置性見長(zhǎng)的Spring,怎么在這一個(gè)小小的環(huán)節(jié)上就不能做得再靈活些呢,一定要一個(gè)個(gè)手寫Class路徑么?
估計(jì)有不少人無奈選擇了從AnnotationSessionFactoryBean繼承一個(gè)自定義的子類,自己實(shí)現(xiàn)掃描邏輯,找出@Entity注解過的類清單配置進(jìn)去。
Spring 2.5.6里有個(gè)不怎么起眼的改進(jìn),那就是在AnnotationSessionFactoryBean上增加了一個(gè)新的方法:
setPackagesToScan(String[] packagesToScan) 有了這個(gè)方法,我們不再需要自己動(dòng)手去實(shí)現(xiàn)實(shí)體類的掃描了,直接在Spring配置文件中AnnotationSessionFactoryBean這個(gè)section上增加類似如下的一個(gè)property即可(假定你需要加載的實(shí)體類所在的包名match這個(gè)字符串"com.**.bo"):
< property? name ="packagesToScan" ?value ="com.**.bo" />
你也可以以清單的方式指定多于1條的匹配字串,如:
< property? name ="packagesToScan" > ????< list > ????????< value > com.abc.core.bo </ value > ????????< value > com.abc.auditing.bo </ value > ????</ list > </ property >
Pylons是一個(gè)典型的MVC Web框架,在之前的幾篇隨筆中,我們一起了解了Pylons的安裝、默認(rèn)項(xiàng)目結(jié)構(gòu)、Routes和controller("C")以及SQLAlchemy("M"),在這個(gè)系列的最后,我們一起來看看"V"。
在我們的controller代碼中,每個(gè)action在return的時(shí)候,可以選擇:
1- 直接return字符串
2- 通過render()函數(shù)將輸出交給頁(yè)面模板引擎處理
3- redirect_to()重定向到其他URL
直接return太簡(jiǎn)單,redirect_to也沒有特別需要介紹的,重點(diǎn)看render()。如果你一直follow這個(gè)系列,那么在你的controllers/hello.py中,可以看到這樣一行import:
from ?newapp.lib.base? import ?BaseController,?render
從lib.base引入了一個(gè)render函數(shù),跟到lib/base代碼里查看,我們會(huì)知道:
from ?pylons.templating? import ?render_mako?as?render
其實(shí)我們用到的render()函數(shù),是pylons.templating.render_mako的別名。
注: 這里假定你在paster create時(shí)選擇了默認(rèn)的mako,其他Pylons原生支持的頁(yè)面模板引擎還有結(jié)構(gòu)相對(duì)更層次化的Genshi和更接近Django實(shí)現(xiàn)的Jinja。 render_mako()函數(shù)簽名如下:
render_mako(template_name, extra_vars=None, cache_key=None, cache_type=None, cache_expire=None)
最基本的用法是給出template文件名,然后通過extra_vars傳入?yún)?shù),Pylons默認(rèn)查找頁(yè)面模板文件是在項(xiàng)目的templates子目錄,這個(gè)路徑也可以在config/environment.py中修改。在Pylons中,被推薦的傳參做法是使用tmpl_context,在生成controller的時(shí)候,已經(jīng)自動(dòng)import了tmpl_context并別名為c,這樣,我們只需要在c上綁上我們需要傳遞給模板的數(shù)據(jù),模板在解析時(shí),也就能夠通過c得到這些數(shù)據(jù)了。像這樣:
c.name? = ?u ' Pylons?n00b ' return ?render( ' hello.mako ' )
然后,在hello.mako中:
< h3 > Hello? < b > ${c.name} </ b > ! </ h3 >
在模板代碼中,有些Pylons系統(tǒng)變量/函數(shù)是可以直接訪問的,包括:
tmpl_context (c) - 用于controller和template傳遞數(shù)據(jù)
config - 配置信息
app_globals (g) - 應(yīng)用的全局變量
h - WebHelpers,包括h.form、h.link_to、h.url_for等等實(shí)用函數(shù)
request - 請(qǐng)求
response - 應(yīng)答
session - 會(huì)話信息
translator、ungettext()、_()、N_() - 處理國(guó)際化
除了基本的${}變量替代語(yǔ)法之外,類似JSP,Mako還支持注釋、if/else/for控制邏輯、代碼片段、return、標(biāo)簽等,具體的可以掃一眼官方說明:
http://www.makotemplates.org/docs/syntax.html 很精簡(jiǎn),也非常容易理解,這里就不詳細(xì)說明了。
至此,我們已經(jīng)了解了Pylons最核心的幾個(gè)組件,足夠我們搭建常規(guī)的Web應(yīng)用了。其他值得大家繼續(xù)挖掘的內(nèi)容包括:國(guó)際化、表單驗(yàn)證(FormEncode)、用戶驗(yàn)證和權(quán)限(AuthKit、repoze.who、repoze.what)、AJAX、Python 3.0、WSGI基礎(chǔ)架構(gòu)等。
本文是該系列最后一篇,希望通過簡(jiǎn)單的介紹和學(xué)習(xí),大家能夠喜歡并順利的上手Pylons,也希望越來越多的人關(guān)注這個(gè)優(yōu)秀的Python Web應(yīng)用框架。
在前面的4篇隨筆中,我們簡(jiǎn)要的介紹了SQLAlchemy,不過SQLAlchemy如何被集成到Pylons應(yīng)用中呢?
首先我們看一下自動(dòng)生成代碼中的model子目錄,其中有兩個(gè)文件__init__.py和meta.py,其中meta.py定義了engine、Session和metadata三個(gè)公用變量,而__init__.py提供了一個(gè)核心的init_model(engine)方法,該方法分別將數(shù)據(jù)庫(kù)engine和經(jīng)過sessionmaker和scoped_session包裝的Session對(duì)象植入到meta中,像這樣:
????sm? = ?orm.sessionmaker(autoflush = True,?autocommit = True,?bind = engine) ????meta.engine? = ?engine ????meta.Session? = ?orm.scoped_session(sm)
這樣一來,整個(gè)背后的"magic"就還剩下最后一塊"拼圖":誰(shuí)來把engine初始化好并調(diào)用init_model方法呢?看看config/environment.py就清楚了:
?1 ?""" Pylons?environment?configuration """ ?2 ?import ?os ?3 ??4 ?from ?mako.lookup? import ?TemplateLookup ?5 ?from ?pylons.error? import ?handle_mako_error ?6 ?from ?pylons? import ?config ?7 ?from ?sqlalchemy? import ?engine_from_config ?8 ??9 ?import ?newapp.lib.app_globals?as?app_globals 10 ?import ?newapp.lib.helpers 11 ?from ?newapp.config.routing? import ?make_map 12 ?from ?newapp.model? import ?init_model 13 ?14 ?def ?load_environment(global_conf,?app_conf): 15 ????? """ Configure?the?Pylons?environment?via?the?``pylons.config`` 16 ?????object 17 ????? """ 18 ????? # ?Pylons?paths 19 ?????root? = ?os.path.dirname(os.path.dirname(os.path.abspath( __file__ ))) 20 ?????paths? = ?dict(root = root, 21 ??????????????????controllers = os.path.join(root,? ' controllers ' ), 22 ??????????????????static_files = os.path.join(root,? ' public ' ), 23 ??????????????????templates = [os.path.join(root,? ' templates ' )]) 24 ?25 ????? # ?Initialize?config?with?the?basic?options 26 ?????config.init_app(global_conf,?app_conf,?package = ' newapp ' ,?paths = paths) 27 ?28 ?????config[ ' routes.map ' ]? = ?make_map() 29 ?????config[ ' pylons.app_globals ' ]? = ?app_globals.Globals() 30 ?????config[ ' pylons.h ' ]? = ?newapp.lib.helpers 31 ?32 ????? # ?Create?the?Mako?TemplateLookup,?with?the?default?auto-escaping 33 ?????config[ ' pylons.app_globals ' ].mako_lookup? = ?TemplateLookup( 34 ?????????directories = paths[ ' templates ' ], 35 ?????????error_handler = handle_mako_error, 36 ?????????module_directory = os.path.join(app_conf[ ' cache_dir ' ],? ' templates ' ), 37 ?????????input_encoding = ' utf-8 ' ,?output_encoding = ' utf-8 ' , 38 ?????????imports = [ ' from?webhelpers.html?import?escape ' ], 39 ?????????default_filters = [ ' escape ' ]) 40 ????? 41 ????? # ?Setup?SQLAlchemy?database?engine 42 ?????engine? = ?engine_from_config(config,? ' sqlalchemy. ' ) 43 ?????init_model(engine) 44 ????? 45 ????? # ?CONFIGURATION?OPTIONS?HERE?(note:?all?config?options?will?override 46 ????? # ?any?Pylons?config?options)
注意第7行的import和第42、43行代碼,是不是豁然開朗?Pylons在初始化運(yùn)行環(huán)境時(shí),從config中讀取sqlalchemy相關(guān)的配置信息,然后通過這些配置信息創(chuàng)建數(shù)據(jù)庫(kù)engine,并調(diào)用init_model()方法初始化SQLAlchemy功能的核心對(duì)象:metadata和Session。有了meta.Session,我們就可以方便的在代碼中執(zhí)行對(duì)model層/數(shù)據(jù)庫(kù)的訪問了。
接著前面的例子說,我們定義了book_table和author_table,接下來:
?1 ?class ?Book(object): ?2 ????? def ? __init__ (self,?title): ?3 ?????????self.title? = ?title ?4 ????? def ? __repr__ (self): ?5 ????????? return ? " <Book('%s')> " ? % ?self.title ?6 ??7 ?class ?Author(object): ?8 ????? def ? __init__ (self,?name): ?9 ?????????self.name? = ?name 10 ????? def ? __repr__ (self): 11 ????????? return ? " <Author('%s')> " ? % ?self.name
這里我們定義兩個(gè)類,繼承自object,類似JavaBeans或者POJO,這里的__init__方法和__repr__方法不是必須的,只是為了創(chuàng)建對(duì)象和輸出對(duì)象內(nèi)容比較方便。然后就可以用SQLAlchemy的mapper和sessionmaker來建立映射關(guān)系并處理持久和查詢等操作:
?1 ?from ?sqlalchemy.orm? import ?mapper,sessionmaker ?2 ??3 ?mapper(Book,?book_table) ?4 ?mapper(Author,?author_table) ?5 ??6 ?Session? = ?sessionmaker(bind = engine) ?7 ?session? = ?Session() ?8 ??9 ?gia? = ?Book(u ' Groovy?in?Action ' ) 10 ?ag? = ?Author(u ' Andrew?Glover ' ) 11 ?12 ?session.add(gia) 13 ?session.add(ag) 14 ?session.add_all([Book( ' Hibernate?in?Action ' ),?Author( ' Gavin?King ' )]) 15 ?s_gia? = ?session.query(Book).filter_by(title = u ' Groovy?in?Action ' ).first() 16 ?s_gia.title? = u' Groovy?in?Action?Updated ' 17 ?18 ?print ? " [DIRTY] " ,?session.dirty 19 20 ?session.commit() # or session.rollback()
如果你用過Hibernate,那么這些代碼對(duì)你來說,理解起來應(yīng)該沒有任何難度。
假如我告訴你,每次都要像這樣先定義Table(schema),再定義class,然后用mapper建立對(duì)照,是不是有點(diǎn)那啥?SQLAlchemy的開發(fā)者們也意識(shí)到這一點(diǎn),所以從0.5開始,SQLAlchemy可以通過sqlalchemy.ext.declarative支持我們實(shí)現(xiàn)更緊湊的model/schema定義:
?1 ?from ?sqlalchemy.schema? import ?Table,?Column,?ForeignKey,?Sequence ?2 ?from ?sqlalchemy.types? import ? * ?3 ?from ?sqlalchemy.orm? import ?relation ?4 ?from ?sqlalchemy.ext.declarative? import ?declarative_base ?5 ??6 ?Base? = ?declarative_base() ?7 ?metadata? = ?Base.metadata ?8 ??9 ?bookauthor_table? = ?Table( ' bookauthor ' ,?metadata, 10 ?????Column( ' book_id ' ,?Integer,?ForeignKey( ' book.id ' ),?nullable = False), 11 ?????Column( ' author_id ' ,?Integer,?ForeignKey( ' author.id ' ),?nullable = False), 12 ?) 13 ?14 ?class ?Book(Base): 15 ????? __tablename__ ? = ? ' book ' 16 ?????id? = ?Column(Integer,?Sequence( ' seq_pk ' ),?primary_key = True) 17 ?????title? = ?Column(Unicode( 255 ),?nullable = False) 18 ?????authors? = ?relation( ' Author ' ,?secondary = bookauthor_table) 19 ?20 ?21 ?class ?Author(Base): 22 ????? __tablename__ ? = ? ' author ' 23 ?????id? = ?Column(Integer,?Sequence( ' seq_pk ' ),?primary_key = True) 24 ?????name? = ?Column(Unicode( 255 ),?nullable = False) 25 ?????books? = ?relation( ' Book ' ,?secondary = bookauthor_table)
這里我們用到了many-to-many關(guān)系,其他的常見用法還包括many-to-one、one-to-many、JOIN、子查詢、EXISTS、Lazy/Eager Load、Cascade (all/delete/delete-orphan)等等,大家可以根據(jù)需要查閱官方文檔。
在介紹SQLAlchemy最核心最有價(jià)值的ORM部分之前,我們?cè)俸?jiǎn)單過一遍SQLAlchemy提供的SQL Expression Language用法,就從最基本的CRUD來舉例說明吧(接著上一篇的示例):
?1 ?from ?sqlalchemy? import ?select,update,delete ?2 ??3 ?conn? = ?engine.connect() ?4 ?book_ins? = ?book_table.insert(values = dict(title = u ' Groovy?in?Action ' )) ?5 ?author_ins? = ?author_table.insert(values = dict(name = u ' Andrew?Glover ' )) ?6 ?conn.execute(book_ins) ?7 ?conn.execute(author_ins) ?8 ?book? = ?conn.execute(select([book_table],?book_table.c.title.like(u ' Groovy% ' ))).fetchone() ?9 ?author? = ?conn.execute(select([author_table])).fetchone() 10 ?bookauthor_ins? = ?bookauthor_table.insert(values = dict(book_id = book[0],author_id = author[0])) 11 ?conn.execute(bookauthor_ins) 12 ?conn.execute(update(book_table,book_table.c.title == u ' Groovy?in?Action ' ),?title = u ' Groovy?in?Action?(中文版) ' ) 13 ?conn.execute(delete(bookauthor_table)) 14 ?conn.close()
簡(jiǎn)單說明一下代碼邏輯:
首先從engine建立連接,然后做兩個(gè)insert動(dòng)作,分別insert一條book記錄(title為'Groovy in Action')和一條author記錄(name為'Andrew Glover'),這之后分別再做兩次select,得到剛insert的這兩條記錄,其中book記錄的select用到了過濾條件,相當(dāng)于"WHERE book.title like 'Groovy%'",然后構(gòu)建一條新的insert語(yǔ)句,用于insert一條bookauthor關(guān)系記錄,接下來,做一次update,將book.title為'Groovy in Action'的更新為'Groovy in Action (中文版)',最后,在關(guān)閉連接之前,做一次delete,刪除bookauthor中的記錄。
在指定WHERE條件時(shí),.c是.columns的簡(jiǎn)寫,所以book_table.c.title指代的就是book表的title列。更高級(jí)的用法是采用"&"、"|"、"!"三個(gè)符號(hào),分別表示AND、OR和NOT,加上必要的"("和")"實(shí)現(xiàn)復(fù)雜的條件定義。由于傳遞給select()的第一個(gè)參數(shù)是個(gè)list,所以你應(yīng)該已經(jīng)猜到了,我們也可以多張表做關(guān)聯(lián)查詢。
在sqlalchemy.schema和sqlalchemy.types這兩個(gè)module中,包含了定義數(shù)據(jù)庫(kù)schema所需要的所有類,如Table、Column、String、Text、Date、Time、Boolean等。還是來看一個(gè)例子:
?1 ?from ?sqlalchemy.engine? import ?create_engine ?2 ?from ?sqlalchemy.schema? import ?MetaData,?Table,?Column,?ForeignKey,?Sequence ?3 ?from ?sqlalchemy.types? import ? * ?4 ??5 ?engine? = ?create_engine( ' postgres://test:test@localhost/test ' ,?echo = True) ?6 ??7 ?metadata? = ?MetaData() ?8 ?metadata.bind? = ?engine ?9 ?10 ?book_table? = ?Table( ' book ' ,?metadata, 11 ?????Column( ' id ' ,?Integer,?Sequence( ' seq_pk ' ),?primary_key = True), 12 ?????Column( ' title ' ,?Unicode( 255 ),?nullable = False), 13 ?) 14 ?15 ?author_table? = ?Table( ' author ' ,?metadata, 16 ?????Column( ' id ' ,?Integer,?Sequence( ' seq_pk ' ),?primary_key = True), 17 ?????Column( ' name ' ,?Unicode( 255 ),?nullable = False), 18 ?) 19 ?20 ?bookauthor_table? = ?Table( ' bookauthor ' ,?metadata, 21 ??? Column( ' book_id ' ,?Integer,?ForeignKey( ' book.id ' ),?nullable = False), 22 ??? Column( ' author_id ' ,?Integer,?ForeignKey( ' author.id ' ),?nullable = False), 23 ) 24 25 metadata.create_all(checkfirst = True)
首先我們還是create_engine,然后新建一個(gè)MetaData對(duì)象,把engine綁上去,接下來,開始在metadata中定義表結(jié)構(gòu)(metadata由Table構(gòu)造函數(shù)傳入),我們這里定義了3張表,分別是book、author和bookauthor關(guān)系表(“多對(duì)多”),其中新建一個(gè)Sequence對(duì)象,專門處理主鍵生成。最后我們通過執(zhí)行metadata.create_all()創(chuàng)建數(shù)據(jù)庫(kù)表,參數(shù)checkfirst=True表示如果數(shù)據(jù)庫(kù)相關(guān)對(duì)象已經(jīng)存在,則不重復(fù)執(zhí)行創(chuàng)建。
對(duì)于已經(jīng)存在于數(shù)據(jù)庫(kù)中的表,我們可以通過傳入autoload=True參數(shù)到Table構(gòu)造函數(shù)的方式來加載現(xiàn)有的表結(jié)構(gòu)到metadata中,而不必挨個(gè)兒再寫一遍Column清單。
看到這兒,你也許覺得挺麻煩,不是么?Django和RoR都是可以直接定義數(shù)據(jù)model類,順帶就把schema也定義了,而不是像這樣單獨(dú)去寫表結(jié)構(gòu)的schema,顯得很"底層"。確實(shí),這樣用SQLAlchemy并不是最優(yōu)化的,SQLAlchemy本身并不會(huì)自動(dòng)的幫你做很多事,但它基礎(chǔ)打得很牢。如果你感興趣,也可以先去看一下SQLAlchemy的擴(kuò)展模塊Elixir,通過Elixir,你可以像Ruby on Rails那樣定義出實(shí)體和關(guān)系("Active Record")。
在稍后的第4部分中,我們會(huì)去了解如何以聲明方式來更緊湊的定義我們的model/schema(0.5新特性)。鑒于筆者傾向于更強(qiáng)的控制力,所以在這個(gè)系列中就不去介紹SQLAlchemy的其他擴(kuò)展模塊了,如Elixir、SQLSoup等,大家可以根據(jù)需要去找下官方文檔。
ORM是個(gè)大話題,大到可能好幾本書都說不完。SQLAlchemy,別看它剛出到0.5.2,已然是Python世界ORM的事實(shí)標(biāo)準(zhǔn),受到眾多開發(fā)者和無數(shù)框架的青睞。
如果之前沒有或很少接觸SQLAlchemy,那么學(xué)習(xí)Pylons可能有相當(dāng)一部分時(shí)間都會(huì)花在SQLAlchemy上。通常,人們選擇Pylons或者TurboGears而不是Django,SQLAlchemy在這些決定的背后有著很重的分量。作為Pylons學(xué)習(xí)筆記的一部分,接下來我將分4篇隨筆介紹SQLAlchemy,分別是Engine API、Schema Management (MetaData/Types)、SQL Expression Language和Object Relational Mapper。此文為第1篇,重點(diǎn)介紹Engine API。
類似Java的JDBC,Python也有一個(gè)類似的數(shù)據(jù)庫(kù)訪問接口規(guī)范,那就是DB-API(目前是2.0),不同的常見RDBMS都有符合DB-API標(biāo)準(zhǔn)的Python庫(kù),比如PostgreSQL有psycopg2,Oracle有cx_Oracle等。有了這個(gè)基礎(chǔ),SQLAlchemy也就得以方便的通過DB-API連接不同的數(shù)據(jù)庫(kù)。
以PostgreSQL為例,通過SQLAlchemy的Engine API訪問數(shù)據(jù)庫(kù)的代碼可以這樣來寫:
?1 ?from ?sqlalchemy.engine? import ?create_engine ?2 ??3 ?engine? = ?create_engine( ' postgres://user:pass@localhost/testdb ' ) ?4 ?connection? = ?engine.connect() ?5 ?connection.execute( ?6 ????? """ ?7 ?????CREATE?TABLE?book ?8 ?????( ?9 ?????? id?serial?NOT?NULL, 10 ??????title?character?varying(30)?NOT?NULL, 11 ??????CONSTRAINT pk_book PRIMARY?KEY?(id) 12 ?????); 13 ??? """ 14 ?) 15 ?connection.execute( 16 ????? """ 17 ?????INSERT?INTO?book?(title)?VALUES?(%s); 18 ????? """ , 19 ????? " The?Art?of?UNIX?Programming " 20 ?) 21 ?rs? = ?connection.execute( " SELECT title FROM book " ) 22 ?for ?row? in ?rs: 23 ????? print ? " Book?Title:? " ,?row[ ' title ' ] 24 ?connection.close()
基本步驟就是create_engine、connect、execute和close,沒有很特別的地方,不過SQLAlchemy的Engine API,并不是簡(jiǎn)單的DBAPI調(diào)用,而是包裝了其他內(nèi)容,如數(shù)據(jù)庫(kù)連接池,我們可以在create_engine的時(shí)候指定連接池參數(shù)和其他額外配置,類似這樣:
engine? = ?create_engine( ' postgres://user:pass@localhost/testdb ' ,?pool_size = 10, convert_unicode=True )
類似Spring的JdbcTemplate,更多的時(shí)候,統(tǒng)一使用SQLAlchemy的Engine API而不是DB-API能給我們帶來更大的靈活性,通常也更方便、更安全。
在開始之前,說點(diǎn)提外話,隨著對(duì)Pylons了解的深入,你可能時(shí)不時(shí)需要看看相關(guān)組件/軟件包是否有更新出來,方法也不復(fù)雜,通過"easy_install -U [組件名]"即可,在學(xué)習(xí)或者是開發(fā)過程中,最好是保持環(huán)境相對(duì)較新,直到出現(xiàn)相對(duì)大的release或者即將進(jìn)入產(chǎn)品部署階段。
繼續(xù)介紹Pylons組件,先看個(gè)例子。首先用"paster controller hello"增加一個(gè)controller,路徑中會(huì)增加出以下兩個(gè)文件:
controllers/hello.py tests/functional/test_hello.py
分別對(duì)應(yīng)新增的controller類HelloController和功能測(cè)試類TestHelloController,它們分別繼承自WSGIController->BaseController和TestCase->TestController。
我們主要看hello.py,默認(rèn)內(nèi)容如下:
?1 ?import ?logging ?2 ??3 ?from ?pylons? import ?request,?response,?session,?tmpl_context?as?c ?4 ?from ?pylons.controllers.util? import ?abort,?redirect_to ?5 ??6 ?from ?newapp.lib.base? import ?BaseController,?render ?7 ?# from?newapp?import?model ?8 ??9 ?log? = ?logging.getLogger( __name__ ) 10 ?11 ?class ?HelloController(BaseController): 12 ?13 ????? def ?index(self): 14 ????????? # ?Return?a?rendered?template 15 ????????? # ???return?render('/template.mako') 16 ????????? # ?or,?Return?a?response 17 ????????? return ? ' Hello?World '
如果你的服務(wù)器沒有Ctrl-C停掉,那么這個(gè)時(shí)候你已經(jīng)可以通過
http://127.0.0.1:5000/hello/index 看到該controller的處理結(jié)果了(Hello World)。
簡(jiǎn)單改造一下17行:
???????? from ?pylons? import ?config ???????? return ? ' <br/> ' .join(config.keys())
我們就可以在返回頁(yè)面上顯示出所有可以通過pylons.config訪問到的參數(shù)列表。出了返回文本,也可以通過render()方法交給頁(yè)面模板引擎生成頁(yè)面,也可以通過redirect_to()跳轉(zhuǎn)到其他URL。
Pylons是如何找到該請(qǐng)求應(yīng)該由HelloController的index方法來處理的呢?這背后發(fā)生了什么?答案就是Routes。
Routes的作者是Ben Bangert,是Pylons框架三個(gè)主要作者/維護(hù)者之一,早期的版本主要是仿照Ruby on Rails的routes.rb開發(fā)的,有RoR經(jīng)驗(yàn)的朋友可能一眼就能發(fā)現(xiàn)它們之間的相似之處。目前Routes的最新版是1.10.2。
Pylons應(yīng)用中,routing的配置在config/routing.py,默認(rèn)生成的內(nèi)容如下:
?1 ?""" Routes?configuration ?2 ??3 ?The?more?specific?and?detailed?routes?should?be?defined?first?so?they ?4 ?may?take?precedent?over?the?more?generic?routes.?For?more?information ?5 ?refer?to?the?routes?manual?at?http://routes.groovie.org/docs/ ?6 ?""" ?7 ?from ?pylons? import ?config ?8 ?from ?routes? import ?Mapper ?9 ?10 ?def ?make_map(): 11 ????? """ Create,?configure?and?return?the?routes?Mapper """ 12 ?????map? = ?Mapper(directory = config[ ' pylons.paths ' ][ ' controllers ' ], 13 ??????????????????always_scan = config[ ' debug ' ]) 14 ?????map.minimization? = ?False 15 ????? 16 ????? # ?The?ErrorController?route?(handles?404/500?error?pages);?it?should 17 ????? # ?likely?stay?at?the?top,?ensuring?it?can?always?be?resolved 18 ?????map.connect( ' /error/{action} ' ,?controller = ' error ' ) 19 ?????map.connect( ' /error/{action}/{id} ' ,?controller = ' error ' ) 20 ?21 ????? # ?CUSTOM?ROUTES?HERE 22 ?23 ?????map.connect( ' /{controller}/{action} ' ) 24 ?????map.connect( ' /{controller}/{action}/{id} ' ) 25 ?26 ????? return ?map
在這個(gè)配置中,對(duì)我們剛才的實(shí)例起到?jīng)Q定性作用的是第23行,我們的輸入U(xiǎn)RL為"http://127.0.0.1:5000/hello/index",其中"/hello/index"通過"/{controller}/{action}"這個(gè)表達(dá)式match出controller為hello而action為index的解析結(jié)果,從而在controllers目錄找到hello.py,和其中HelloController的index方法,進(jìn)行調(diào)用。
map.connect()在上面代碼中體現(xiàn)出兩種用法:
map.connect('pattern', key=value) - 指定默認(rèn)的controller、action、id等
map.connect('pattern') - 直接指定pattern
pattern字符串允許通配符,通常在最后一個(gè)元素上,比如'/{controller}/{action}/{*url}',將后面的整個(gè)URL片段交給前面指定的controller/action處理。除此以外,map.connect()還支持
1- "路徑別名",如:
map.connect('name', 'pattern', [_static=True])
如果_static設(shè)為"True",表示為"靜態(tài)命名路徑"。
2- 額外的匹配條件,如:
map.connect('/{controller}/{action}/{id}', requirements={'year': '\d+',})
map.connect('/{controller}/{action}/{id}', conditions=dict(method=['GET','POST']))
所有的route優(yōu)先級(jí)為從上到下。Routes除了提供解析進(jìn)來的URL的邏輯,在我們的controller和template代碼中,我們還可以方便的通過WebHelpers的url_for()方法計(jì)算相應(yīng)的URL。
Routes 1.x中的有一些仿routes.rb功能將會(huì)在2.0中被去掉,包括Route Minimization、Route Memory、Implicit Defaults等。如果有興趣的話,可以參考一下官方文檔,這里就不一一介紹了。為什么要去掉?當(dāng)然主要的動(dòng)機(jī)還是減少歧義,避免一些不必要的混淆。至于深層次的原因么,可以參考Tim Peters《The Zen of Python》中的一句經(jīng)典的Python哲學(xué):Explicit is better than implicit。什么?沒有聽說過?打開python命令行,輸入"import this"后回車,慢慢體會(huì)其中的道理吧。:)
大家新年好!
在
前一篇隨筆 中,大家了解了什么是Pylons,有哪些特點(diǎn),今天筆者繼續(xù)給介紹默認(rèn)生成的項(xiàng)目結(jié)構(gòu)。
通過Paste創(chuàng)建新的Pylons應(yīng)用很簡(jiǎn)單,就是一句"paster create -t pylons [應(yīng)用名]"命令,其中"-t pylons"或者全稱"--template=pylons",用以告訴Paste我們新建的項(xiàng)目,將是一個(gè)Pylons應(yīng)用,或者說,從一個(gè)預(yù)定義好的Pylons默認(rèn)項(xiàng)目模板生成。如果你愿意,你也可以自己來制作新的模板以符合需要。"paster create --list-templates"可以查看當(dāng)前可用的模板。
假定我們新建的Pylons項(xiàng)目名稱為NewApp,那么執(zhí)行"paster create -t pylons NewApp"后,我們將得到一個(gè)新的NewApp目錄,其中包含一個(gè)docs目錄(文檔)、一個(gè)newapp目錄(代碼)、一個(gè)NewApp.egg-info目錄(元數(shù)據(jù))和一些頂級(jí)配置文件。
在開發(fā)過程中,我們經(jīng)常會(huì)用到的是代碼目錄(這里是newapp)和位于項(xiàng)目根目錄下的development.ini和test.ini文件了。注意這里newapp的大小寫,Pylons在生成項(xiàng)目的時(shí)候,不論你指定的項(xiàng)目名稱大小寫是怎樣,這里會(huì)自動(dòng).lower()轉(zhuǎn)小寫,只有項(xiàng)目頂級(jí)路徑和egg信息會(huì)保留原始大小寫,筆者認(rèn)為這更加符合Web應(yīng)用的nature。
代碼目錄下包括config、controllers、lib、model、public、templates和tests子目錄,分別用于存放配置(如環(huán)境、中間件、路徑查找邏輯)、控制器(處理請(qǐng)求)、全局輔助代碼(全局變量、helpers等)、模型(后臺(tái)數(shù)據(jù)訪問)、靜態(tài)頁(yè)面/媒體文件、頁(yè)面模板和測(cè)試代碼。
最后說說ini文件:默認(rèn)生成的development.ini和test.ini顧名思義分別對(duì)應(yīng)開發(fā)和測(cè)試需要的基本配置,我們可以通過修改相應(yīng)的參數(shù)配置來指定具體設(shè)定,如服務(wù)器IP和端口、數(shù)據(jù)庫(kù)連接、日志等。看到這里你也許會(huì)問,那么產(chǎn)品環(huán)境呢?答案是默認(rèn)沒有生成,你可以從development.ini修改成需要的產(chǎn)品環(huán)境配置,也可以通過paster make-config newapp production.ini生成一個(gè)默認(rèn)的產(chǎn)品環(huán)境典型配置。
以development.ini為例,Pylons的ini文件主要包括4個(gè)部分的內(nèi)容:
1- 全局默認(rèn)參數(shù),如
[ DEFAULT ] debug?= ?true #?Uncomment?and?replace?with?the?address?which?should?receive?any?error?reports #email_to? = ?you@yourdomain.com smtp_server? = ?localhost error_email_from? = ?paste@localhost
2- 服務(wù)器配置,如
[ server:main ] use?= ?egg:Paste#http host? = ? 127.0.0.1 port?= ? 5000
3- 應(yīng)用程序配置,如
[ app:main ] use?= ?egg:NewApp full_stack? = ?true cache_dir? = ?%(here)s/data beaker.session.key? = ?newapp beaker.session.secret? = ?somesecret #?If?you'd?like?to?fine-tune?the?individual?locations?of?the?cache?data?dirs #?for?the?Cache?data , ?or?the?Session?saves , ?un-comment?the?desired?settings #?here: #beaker.cache.data_dir? = ?%(here)s/data/cache #beaker.session.data_dir? = ?%(here)s/data/sessions #?SQLAlchemy?database?URL sqlalchemy.url? = ?sqlite:///%(here)s/development.db #?WARNING:?*THE?LINE?BELOW?MUST?BE?UNCOMMENTED?ON?A?PRODUCTION?ENVIRONMENT* #?Debug?mode?will?enable?the?interactive?debugging?tool , ?allowing?ANYONE?to #?execute?malicious?code?after?an?exception?is?raised. #set?debug? = ?false
簡(jiǎn)單說明一下,這里的full_stack設(shè)置為true表示打開交互式調(diào)試和錯(cuò)誤報(bào)告等功能,"%(here)s"會(huì)被替換成項(xiàng)目所在路徑,類似相對(duì)路徑url中的"."轉(zhuǎn)絕對(duì)路徑,而beaker.*為配置會(huì)話相關(guān)的設(shè)定,如緩存、cookie基本內(nèi)容等。對(duì)于產(chǎn)品環(huán)境來說,比較重要的一個(gè)配置是"set debug=false",否則一旦出現(xiàn)異常,交互式調(diào)試將使得攻擊者能夠執(zhí)行系統(tǒng)命令。
4- 日志,如
[ loggers ] keys?= ?root , ?routes , ?newapp , ?sqlalchemy [ handlers ] keys?= ?console [ formatters ] keys?= ?generic [ logger_root ] level?= ?INFO handlers? = ?console [ logger_routes ] level?= ?INFO handlers? = qualname?= ?routes.middleware #? " level?=?DEBUG " ?logs?the?route?matched?and?routing?variables. [ logger_newapp ] level?= ?DEBUG handlers? = qualname?= ?newapp [ logger_sqlalchemy ] level?= ?INFO handlers? = qualname?= ?sqlalchemy.engine #? " level?=?INFO " ?logs?SQL?queries. #? " level?=?DEBUG " ?logs?SQL?queries?and?results. #? " level?=?WARN " ?logs?neither.??(Recommended?for?production?systems.) [ handler_console ] class?= ?StreamHandler args? = ?(sys.stderr , ) level? = ?NOTSET formatter? = ?generic [ formatter_generic ] format?= ?%(asctime)s , %(msecs)03d?%(levelname)- 5 .5s? [ %(name)s ] ?%(message)s datefmt? = ?%H:%M:%S
OK,這一篇就先講到這兒,下一篇將介紹Routes和controller。
Pylons是一個(gè)Python語(yǔ)言的Web應(yīng)用程序框架,如果你簡(jiǎn)單了解過Ruby on Rails和Django,你大概會(huì)問,Pylons有什么不一樣呢?Pylons最大的特點(diǎn)是模塊化,將處理Web應(yīng)用環(huán)境下不同領(lǐng)域、不同問題的軟件包集成在一起,形成一個(gè)整體,在提供一攬子解決方案的同時(shí),不阻礙你選擇別的替代組件。另外,Pylons是目前對(duì)WSGI標(biāo)準(zhǔn)支持最好的框架之一,未來的TurboGears 2.0也會(huì)基于Pylons構(gòu)建。
Pylons從Ruby on Rails借鑒了不少東西,比如Routes,比如WebHelpers,從表面看更像是Python版的RoR,不過底下的架構(gòu)應(yīng)該說更加輕量和靈活,因?yàn)槟憧梢造`活選擇自己熟悉或者更貼和具體應(yīng)用實(shí)際的組件,從ORM到頁(yè)面模板,Pylons只是推薦一些大家普遍比較認(rèn)可的選項(xiàng),但并不強(qiáng)制你使用它們。
說完和Ruby on Rails的異同,當(dāng)然也要回過頭來說說同樣是Python編寫的Django。如果你只是想迅速的構(gòu)建一個(gè)可以支撐大量訪問的Web應(yīng)用,Django是個(gè)不錯(cuò)的選擇,但和RoR一樣,你在很大程度上被限制在一定的pattern中:如果你按照Django的思路去實(shí)現(xiàn)你的應(yīng)用,你會(huì)很happy;但一旦你覺得某個(gè)組件你不喜歡、不符合某個(gè)實(shí)際要求,想要來點(diǎn)定制,你就會(huì)覺得有些伸不開拳腳,或者工程浩大。目前感覺Django比較不爽的地方有:頁(yè)面模板較弱,表現(xiàn)力有些不足,也有人說夠用了;ORM目前是自己的一套,暫時(shí)沒有成熟的SQLAlchemy支持,需要第三方包或者自己做;從架構(gòu)上,Django對(duì)MVC的解讀是MTV(Model-Template-View),大家都叫作controller的東東,在Django的世界里是view,以至于每次和別人解釋,都要多費(fèi)一番口舌。
Pylons目前版本是0.9.7(rc4),主要用到的第三方/獨(dú)立組件有Paste、Routes、Beaker、Mako、FormEncode、WebHelpers和SQLAlchemy。安裝方法如下:
首先你必須有Python(2.3+),然后你可以選擇直接easy_install Pylons或者新建一個(gè)Virtual Environment,和系統(tǒng)中的Python環(huán)境隔離開,依賴的包可以獨(dú)立升級(jí)。這里我們按照后一種方式,如果你是第一次使用Pylons,建議你也在獨(dú)立Python virtualenv中安裝。
1- easy_install virtualenv (這將安裝Python虛擬環(huán)境工具)
2- python virtualenv.py ENV (創(chuàng)建新的虛擬環(huán)境。這里的ENV是你新建虛擬環(huán)境的路徑,如"mydevenv")
3- source ENV/bin/activate (激活虛擬環(huán)境。如果是Windows的話,這里需要執(zhí)行ENV\bin\activate.bat)
4- easy_install Pylons (這里使用的是虛擬環(huán)境的easy_install安裝)
如果你覺得上面的步驟麻煩,Pylons開發(fā)團(tuán)隊(duì)提供了一個(gè)腳本來處理安裝過程,下載后用Python執(zhí)行即可:
http://www.pylonshq.com/download/0.9.7/go-pylons.py 如果需要SQLAlchemy,則再執(zhí)行一下
easy_install SQLAlchemy
安裝成功后,通過
paster create -t pylons [應(yīng)用名]
即可新建Web應(yīng)用主框架,然后cd到應(yīng)用下,通過
paster serve --reload development.ini
啟動(dòng)Web服務(wù),默認(rèn)地址在
http://127.0.0.1:5000/ 更詳細(xì)的信息,可參考Pylons項(xiàng)目主頁(yè):
http://pylonshq.com/ 隨著使用的深入,筆者還會(huì)陸續(xù)對(duì)Pylons和其他相關(guān)組件進(jìn)行進(jìn)一步的介紹。祝各位農(nóng)歷新年快樂!
在Unix環(huán)境下,命令行或者shell中sleep和kill是常見的動(dòng)作,在Windows的.bat文件中處理類似的任務(wù)就不那么直接了,備忘如下: [sleep] ping 127.0.0.1 -n 需要的秒數(shù)+1 -w 1000 > nul [kill] taskkill /f /im "進(jìn)程名(如notepad.exe)" taskkill /f /fi "WINDOWTITLE eq notepad*" 其中/f表示強(qiáng)制,/im表示image鏡像名(可執(zhí)行文件名),/fi表示filter,后面跟表達(dá)式,比如這里的"窗體標(biāo)題等于notepad*",支持wildcast通配符。