castle activerecordd里的attributes属性不管怎么样都是null

Read and Write Activerecord Attribute
上一节讲完了ActiveRecord的对象怎么从是数据库里取出来,但距离数据最终的读写其中还有不少的处理过程。比如模型的属性在读取时需要做出一些相应的转换,同理在修改了模型属性之后回写数据库的时候也需要做转换。另外ActiveRecord使用了Ruby的动态特性为所有的属性读写都生成了与属性名相对应的方法,让开发者能更加便捷地访问所需要的属性值。
首先来看看数据库取出的数据怎样存放到对象中,以下是相应的代码,
instantiate方法的解释请参考
Assemble ActiveRecord Object
# file: active_record/persistence
def instantiate(record, column_types = {})
column_types = self.decorate_columns(column_types.dup)
klass.allocate.init_with('attributes' =& record, 'column_types' =& column_types)
# file: active_record/core_
def init_with(coder)
@attributes
= self.class.initialize_attributes(coder['attributes'])
# 其他初始化过程 bla bla bla
可以看到,数据库里的每条记录从数据库查出来之后,会直接塞进每个对象的
@attributes实例变量中,这里包括了所有的字段的名字和值。这个原始的记录数据是个以属性名为键,原始内容为值的哈希表。
ActiveRecord提供了接口可以直接访问原始数据,这种方式就是直接对
@attributes进行读取。
Post.first.attributes_before_type_cast # 读取所有原始数据Post.first.read_attribute_before_type_cast(:id) # 读取ID字段的原始数据Post.first.id_before_type_cast # 同上,ActiveModel::AttributeMethods生成的DSL
通常我们不会直接访问原始数据,而是访问已经转化好的数据。ActiveRecord提供了几种形式来访问处理过属性
post = Post.new(name: &First Post&)post.namepost[:name]post.attributes[:name]post.read_attribute(:name) #=& &First Post&
以上几种的模型属性访问其实都通过同一个入口进行访问,这个入口就是
read_attribute。以上几个属性读取的实现有兴趣可以自行翻查源码,我们来重点讲解
read_attribute。
read_attribute的基本逻辑如以下代码所示,这里是精简过的代码
# file: active_record/attribute_methods/read.rbdef read_attribute(attr_name)
name = attr_name.to_s
column = @column_types[name]
value = @attributes.fetch(name) {
return block_given? ? yield(name) : nil
column.type_cast valueend
查找对应对应的数据库字段(AR::ConnectionAdapters::Column)实例,即获得该属性在数据库里对应的类型
从原始数据
@attributes里查找出对应的值
使用对应的字段类型来转换该属性的原始值
数据库表与AR对象的映射会在对应的章节里讲解,本篇只讲解和字段读写相关的部分,以下是类型转换的实现
def type_cast(value)
return nil if value.nil?
return coder.load(value) if encoded?
klass = self.class
when :string, :text
then value
when :integer
then klass.value_to_integer(value)
when :float
then value.to_f
when :decimal
then klass.value_to_decimal(value)
when :datetime, :timestamp then klass.string_to_time(value)
when :time
then klass.string_to_dummy_time(value)
when :date
then klass.value_to_date(value)
when :binary
then klass.binary_to_string(value)
when :boolean
then klass.value_to_boolean(value)
else value
我们看到除了字符串和文本之外的类型都需要根据其逻辑类型,进行转换的方法主要是解析内容并实例化到对应的类型。
写入属性的情况与读取属性的逻辑基本相同,并且Column里有一个与
type_cast_for_write对应的
type_cast_for_write方法,用来处理写入的类型转换。
在扩展性方面,Postgres的链接代码重写了类型转换方法以支持它丰富的数据类型。
自定义序列化字段
ActiveRecord支持将Ruby对象直接序列化到数据库中,并且可以制定序列化的方式,默认使用的是YAML。
# file: active_record/attribute_methods/serialization.rbdef serialize(attr_name, class_name = Object)
include Behavior
coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
class_name
Coders::YAMLColumn.new(class_name)
self.serialized_attributes = serialized_attributes.merge(attr_name.to_s =& coder)end
在实现上通过Coder这种形式来在属性的读写时,调用Coder的
dump方法进行预先处理。
这里指定的Coder并不需要特定的类型,它只需要实现接受一个参数的
dump方法就可以作为一个Coder。
属性方法的动态生成
ActiveRecord模型利用Ruby的元编程能力,在运行时生成与数据库字段名相对应的读写方法。具体的方式就是使用
method_missing和
respond_to?,在找不到对应的方法时,ActiveRecord会在以上的两个方法里调用
define_attribute_methods去生成
所有的属性读写方法。
define_attribute_methods方法有两个定义,其中一个定义在ActiveRecord::AttributeMethods,另一个定义在ActiveModel::AttributeMethods模组中,其中实质性的定义是在ActiveModel中,ActiveRecord继承并在这之上加了一些线程安全和方法是否已经生成的标记。
# file: active_model/attribute_methodsdef define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }end
ActiveRecord里无需参数的定义主要作用只是代理,将所有的字段名字传入到ActiveModel里的
define_attribute_methods。然后遍历所有的属性名,将每个属性都传入
define_attribute_method里。
define_attribute_method方法比较复杂,基本的思路是遍历所有的AttributeMethodMatcher,并从Matcher拼装出需要调用的方法名。
这里稍微解释一下AttributeMethodMetcher,所有模型的父类ActiveRecord::Base定义了一堆的Metcher,它用来为所有属性添加方法。除了上面的读写方法和原数据访问方法外,ActiveRecord模型还定义了如下一堆属性相关的方法
post = Post.new title: &Nice Post&post.titlepost.title?post.title_before_type_castpost.title_changed?post.title_changepost.title_will_change!post.title_was
这类方法的定义就是通过Metcher,举个栗子,
{attribute}_before_type_cast是这么定义的
attribute_method_suffix &_before_type_cast&#=& #&ActiveModel::AttributeMethods::ClassMethods::AttributeMethodMatcher:0x007fb36c41ddf0#
@method_missing_target=&attribute_before_type_cast&,#
@method_name=&%s_before_type_cast&,#
@prefix=&&,#
@regex=/^(?:)(.*)(?:_before_type_cast)$/,#
@suffix=&_before_type_cast&&
通过这样的定义,前文提到的
define_attribute_method的时候会调用到上面这个Matcher,然后通过
method_missing_target调用
attribute_before_type_cast去定义模型的
title_before_type_cast。
同时在方法未定义的检查里也是通过遍历所有Matcher,找出是否为预定义的属性方法。
整个方法生成的故事就如是发展,在遇到未定义的方法的时候,ActiveRecord发现该方法是属性相关的方法,那么遍历所有的属性,再嵌套遍历所有的Matcher去生成所有的属性相关方法。
无相关信息
最新教程周点击榜
微信扫一扫深入理解 yii2的Active Record
yii2 中的 &$model-&attribute() & , &$model-&attributes &, & $model-&attributes= [...], model-&fields(),&$model-&toArray();
以下依次进行剖析:
$this-&attributes()
执行的是model的attributes()方法,返回的数据库的字段数组, yii\db\ActiveRecord&代码如下:
public function attributes()
return array_keys(static::getTableSchema()-&columns);
执行返回结果示例:
array (size=14)
0 =& string 'id' (length=2)
1 =& string 'username' (length=8)
2 =& string 'password_hash' (length=13)
3 =& string 'password_reset_token' (length=20)
4 =& string 'email' (length=5)
5 =& string 'auth_key' (length=8)
6 =& string 'status' (length=6)
7 =& string 'created_at' (length=10)
8 =& string 'updated_at' (length=10)
9 =& string 'password' (length=8)
10 =& string 'role' (length=4)
11 =& string 'access_token' (length=12)
12 =& string 'allowance' (length=9)
13 =& string 'allowance_updated_at' (length=20)
var_dump($this-&attributes) 执行的是函数:&\yii\base\model-&getAttributes(),该函数代码如下
public function getAttributes($names = null, $except = [])
$values = [];
if ($names === null) {
$names = $this-&attributes();
foreach ($names as $name) {
$values[$name] = $this-&$
foreach ($except as $name) {
unset($values[$name]);
也就是说,先通过 &$this-&attributes()方法,得到数据库表的字段数组
然后 依次遍历,查看各个属性,$this-&$name获取各个字段对应的值,而这个访问的是对象的属性,是通过魔术方法__get 从private属性 \yii\db\BaseActiveRecord::$_attributes 获取的,也就是说数组的结构是先用数据库字段作为数组的key, value是从数组&\yii\db\BaseActiveRecord::$_attributes中取值
然后拼起来的数组,也就是只有数据库里面的部分,对于model中定义的成员变量和其他的属性,这里是不做输出的。仅仅是数据库的字段部分。
如果您想得到一些定义的成员变量,但是又不想定义fields那么麻烦,您可以通过
$table_attr = $this-&attributes();
$public_x = [ 'password_repeat'];
$arr = array_merge($table_attr,$public_x );
$model-&getAttributes($arr);
返回您想访问的所有的属性和成员变量(注意:我说的属性,指的是不是成员变量的属性。)
魔术方法从&\yii\db\BaseActiveRecord::$_attributes 中取值,如果不存在,返回null
魔兽方法如下:&\yii\db\BaseActiveRecord::__get & & __sete
public function __get($name)
if (isset($this-&_attributes[$name]) || array_key_exists($name, $this-&_attributes)) {
return $this-&_attributes[$name];
} elseif ($this-&hasAttribute($name)) {
if (isset($this-&_related[$name]) || array_key_exists($name, $this-&_related)) {
return $this-&_related[$name];
$value = parent::__get($name);
if ($value instanceof ActiveQueryInterface) {
return $this-&_related[$name] = $value-&findFor($name, $this);
public function __set($name, $value)
if ($this-&hasAttribute($name)) {
$this-&_attributes[$name] = $
parent::__set($name, $value);
因此,在$_attributes不存在的值,就会被赋值null。
$this-&attributes = array();
这个执行的是
yii\base\model-&setAttributes($values, $safeOnly = true);
代码如下:
public function setAttributes($values, $safeOnly = true)
if (is_array($values)) {
$attributes = array_flip($safeOnly ? $this-&safeAttributes() : $this-&attributes());
foreach ($values as $name =& $value) {
if (isset($attributes[$name])) {
$this-&$name = $
} elseif ($safeOnly) {
$this-&onUnsafeAttribute($name, $value);
也就是块赋值,根据场景定义的字段,而且是只有这些字段赋值。
原理就是 将&
1.过滤出来场景允许的安全属性,然后进行赋值
2.如果存在成员变量,那么首先进行成员变量的赋值。
& 如果成员变量不存在,则会使用到__set魔兽方法,添加到 & yii\db\BaseActiveRecord 的private属性$_attributes 数组中
因此,如果在数据库中有一个字段password,如果你在AR类中也定义了一个password,那么就不会保存到&private属性$_attributes 数组中
当然,这个不会影响到保存,因为$model-&attributes 是通过 $this-&$name 的方式读取,而不是通过&private属性$_attributes 数组读取,
通过&&$this-&$name 的方式读取 ,定义的成员变量也是会保存到数据库中的
造成的后果就是 &private属性$_attributes 数组 没有password这个key,fields函数返回就没有password,进而toArray()返回的也没有password。
4.model-&fields()
这个方法默认的值,是抽象方法 yii\db\BaseActiveRecord 的private属性$_attributes
public function fields()
$fields = array_keys($this-&_attributes);
return array_combine($fields, $fields);
也就是说:通过private &$_attributes 数组的key
$model-&toArray()
这个函数的输出,是将 fields 返回的数组输出,从第4部分,可以看到,fields默认是由&&yii\db\BaseActiveRecord 的private属性$_attributes 的key得到
所以,toArray默认是将$_attributes的值得出,如果想在这里添加类的成员变量,可以fields函数中添加:
public function fields()
$fields = parent::fields();
$fields['password_repeat'] = 'password_repeat';
//$fields['rememberMe'] = function ($model) {
return $model-&rememberMe . ' ' . $model-&password_
$fields数组的key 是属性或成员变量,值也是key &通过$model-&password_repeat得到对应的值
如果想自定义,可以通过函数的方式获取。
当findOne得到一个AR实例后
$_attributes 里面保存的是所有的属性
$_oldAttributes里面保存的也是所有的数据库查询出来的属性,和$_attributes一样
当对这个对象重新赋值,实际上是吧值写入到了$_attributes ,而 $_oldAttributes的值是不变的
在最终的save的时候,查看两个数组的差异,进行update对应的字段,
在这里也就看出,实际上AR读取的字段值,不是AR类的成员变量,而是通过__get &__set方法得到的对应的值
而这个值,就是从$_attributes这个private属性中取到的值。
BaseActiveRecord函数:
public function __set($name, $value)
if ($this-&hasAttribute($name)) {
$this-&_attributes[$name] = $
parent::__set($name, $value);
public function __get($name)
if (isset($this-&_attributes[$name]) || array_key_exists($name, $this-&_attributes)) {
return $this-&_attributes[$name];
} elseif ($this-&hasAttribute($name)) {
if (isset($this-&_related[$name]) || array_key_exists($name, $this-&_related)) {
return $this-&_related[$name];
$value = parent::__get($name);
if ($value instanceof ActiveQueryInterface) {
return $this-&_related[$name] = $value-&findFor($name, $this);
所以,通过
$user = User::findOne(1)得到的model对象,
private $_attributes 存储的是数据库里面的值,以及对其赋值
$user-&fields() 得到的是 $_attributes 里面含有的字段,因此就是所有的数据表字段
&$user-&toArray()得到的是所有数据库的字段,也就是说,在默认情况下 toArray的输出的字段,是由 private &$_attributes字段决定的
当然,如果在model中进行了定义新字段,
public $password_
然后再fields中执行
public function fields() 这个函数中添加
$fields['password_repeat'] = 'password_repeat';
public function fields()
$fields = parent::fields();
$fields['password_repeat'] = 'password_repeat';
//$fields['rememberMe'] = function ($model) {
return $model-&rememberMe . ' ' . $model-&password_
通过asArray就输出了 & password_repeat字段了
对于new的新的AR对象
$model = new Useradmin();
$model-&attributes = $this-&getValues1();
public function getValues1(){
'username' =& 'terry',
'password' =& 'passfdafds',
//'password' =& 'passfdafds',
'password_repeat' =& 'passfdafds',
'email' =& 'email',
'created_at'
=& ' 11:11:11',
'access_token' =& null,
'rememberMe' =& 44,
$model-&attributes = $this-&getValues1(); &这个是块赋值,执行的是setAttributes方法,先通 场景 scrnarios函数 &验证是否是安全属性,
如果是,则把数据插入到 private $_attributes中
var_dump($model-&attributes());
var_dump($model-&toArray());
var_dump($model-&attributes);
array (size=14)
0 =& string 'id' (length=2)
1 =& string 'username' (length=8)
2 =& string 'password_hash' (length=13)
3 =& string 'password_reset_token' (length=20)
4 =& string 'email' (length=5)
5 =& string 'auth_key' (length=8)
6 =& string 'status' (length=6)
7 =& string 'created_at' (length=10)
8 =& string 'updated_at' (length=10)
9 =& string 'password' (length=8)
10 =& string 'role' (length=4)
11 =& string 'access_token' (length=12)
12 =& string 'allowance' (length=9)
13 =& string 'allowance_updated_at' (length=20)
array (size=6)
'username' =& string 'terry' (length=5)
'password' =& string 'passfdafds' (length=10)
'email' =& string 'email' (length=5)
'created_at' =& string ' 11:11:11' (length=19)
'access_token' =& null
'password_repeat' =& string 'passfdafds' (length=10)
array (size=14)
'id' =& null
'username' =& string 'terry' (length=5)
'password_hash' =& null
'password_reset_token' =& null
'email' =& string 'email' (length=5)
'auth_key' =& null
'status' =& null
'created_at' =& string ' 11:11:11' (length=19)
'updated_at' =& null
'password' =& string 'passfdafds' (length=10)
'role' =& null
'access_token' =& null
'allowance' =& null
'allowance_updated_at' =& null因为rememberMe不是安全属性,所以块赋值失败
$model-&attributes() 返回的是数据库中所有的字段数组
$model-&toArray()返回的是 $_attributes 属性数组
$model-&attributes 返回的是 key为&$model-&attributes() 返回的key &,值为&&$_attributes 属性数组的对应值,不存在则为null
对于model的foreach
foreach($model as $k=&$v){
echo $k.&=&&.$v.&&br/&&;
输出的是属性 $model-&attributes &也就是&$model-&getAttributes()函数返回的结果。
对应 model的其他的成员变量是不输出的。需要通过
$model-&xxxx调用
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:608848次
积分:17395
积分:17395
排名:第406名
原创:1132篇
转载:293篇
评论:67条
阅读:2802
(9)(6)(12)(8)(22)(30)(48)(67)(60)(20)(11)(9)(12)(30)(14)(34)(18)(23)(22)(13)(21)(22)(41)(27)(39)(16)(22)(15)(26)(37)(42)(55)(23)(2)(5)(10)(15)(2)(1)(1)(1)(6)(2)(1)(10)(35)(17)(62)(13)(55)(61)(38)(162)(75)5431人阅读
Spring(9)
下面是一段典型的Spring 声明事务的配置:
&bean id=&userDAOProxy&
class=&org.springframework.transaction.interceptor.TransactionProxyFactoryBean&&
&property name=&transactionManager&&
&ref bean=&transactionManager& /&
&/property&
&property name=&target&&
&ref local=&userDAO& /&
&/property&
&property name=&transactionAttributes&&
&prop key=&insert*&&PROPAGATION_REQUIRED&/prop&
&prop key=&get*&&PROPAGATION_REQUIRED,readOnly&/prop&
&prop key=&save*&&PROPAGATION_REQUIRED,-ApplicationException,+BusinessException&/prop&
&/property&
在Spring声明事务中,我们可以自定义方法的哪些Exception需要回滚,哪些Exception可以直接提交。
通过下面的配置:
&prop key=&save*&&PROPAGATION_REQUIRED,-ApplicationException,+BusinessException&/prop&
- 表示抛出该异常时需要回滚
+表示即使抛出该异常事务同样要提交
-ApplicationException :表示抛出ApplicationException 时,事务需要回滚。但不是说只抛出ApplicationException 异常时,事务才回滚,如果程序抛出RuntimeException和Error时,事务一样会回滚,即使这里没有配置。因为Spring中默认对所有的RuntimeException和Error都会回滚事务。
Spring中是如何实现这段逻辑的:
org.springframework.transaction.interceptor.RuleBasedTransactionAttribute.rollbackOn(Throwable ex)
public boolean rollbackOn(Throwable ex) {
if (logger.isTraceEnabled()) {
logger.trace(&Applying rules to determine whether transaction should rollback on & + ex);
RollbackRuleAttribute winner =
int deepest = Integer.MAX_VALUE;
//配置文件中的回滚异常列表,当然去掉了-,只有name,commit的规则是另外一个对象
if (this.rollbackRules != null) {
for (RollbackRuleAttribute rule : this.rollbackRules) {
//使用抛出exception的className(全路径className)进行indexOf match
//如果没有match上会继续搜索superClass name进行match,到Throwable class为止
int depth = rule.getDepth(ex);
if (depth &= 0 && depth & deepest) {
if (logger.isTraceEnabled()) {
logger.trace(&Winning rollback rule is: & + winner);
// User superclass behavior (rollback on unchecked) if no rule matches.
if (winner == null) {
logger.trace(&No relevant rollback rule found: applying default rules&);
//如果没有match上,调用此方法继续match,判断instance of RuntimeException or Error
return super.rollbackOn(ex);
return !(winner instanceof NoRollbackRuleAttribute);
rule.getDepth方法代码
& & & & 因为使用的是className的全路径进行indexOf匹配,所以如果自定义异常是:com.abc.ApplicationException,你在xml配置文件中定义为:-abc,同样会match上,事务也会回滚,这一点需要注意。
& & & &另外一点,如何在xml中定义的是-Exception,这样只要class的全路径中包含Exception字段,如包名,也会匹配上。
public int getDepth(Throwable ex) {
return getDepth(ex.getClass(), 0);
private int getDepth(Class exceptionClass, int depth) {
if (exceptionClass.getName().indexOf(this.exceptionName) != -1) {
// Found it!
// If we've gone as far as we can go and haven't found it...
if (exceptionClass.equals(Throwable.class)) {
return -1;
return getDepth(exceptionClass.getSuperclass(), depth + 1);
super.(&ex)&方法代码
很简单的一行代码,这就是为什么RuntimeException和Error也会回滚啦。
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
几次测试输出的debug日志:
[08/24 03:35:39] [DEBUG] RuleBasedTransactionAttribute: Applying rules to determine whether transaction should rollback on usertest.exception.BusinessException: Error
[08/24 03:35:39] [DEBUG] RuleBasedTransactionAttribute: Winning rollback rule is: RollbackRuleAttribute with pattern [BusinessException]
[08/24 03:35:39] [DEBUG] DataSourceTransactionManager: Triggering beforeCompletion synchronization
[08/24 03:35:39] [DEBUG] DataSourceTransactionManager: Initiating transaction rollback
[08/24 03:35:39] [DEBUG] DataSourceTransactionManager: Rolling back JDBC transaction on Connection [mons.dbcp.PoolableConnection@1db05b2]
[08/24 03:35:39] [DEBUG] DataSourceTransactionManager: Triggering afterCompletion synchronization
[08/24 03:35:39] [DEBUG] TransactionSynchronizationManager: Clearing transaction synchronization
[08/24 03:35:39] [DEBUG] TransactionSynchronizationManager: Removed value [org.springframework.jdbc.datasource.ConnectionHolder@1833eca] for key [mons.dbcp.BasicDataSource@4aa0ce] from thread [main]
[08/24 03:35:39] [DEBUG] DataSourceTransactionManager: Releasing JDBC Connection [mons.dbcp.PoolableConnection@1db05b2] after transaction
[08/24 03:35:39] [DEBUG] DataSourceUtils: Returning JDBC Connection to DataSource
[08/24 03:39:16] [DEBUG] TransactionInterceptor: Completing transaction for [usertest.dao.UsersDAO.testInsertAndUpdate] after exception: java.lang.Exception: Error
[08/24 03:39:16] [DEBUG] RuleBasedTransactionAttribute: Applying rules to determine whether transaction should rollback on java.lang.Exception: Error
[08/24 03:39:16] [DEBUG] RuleBasedTransactionAttribute: Winning rollback rule is: null
[08/24 03:39:16] [DEBUG] RuleBasedTransactionAttribute: No relevant rollback rule found: applying superclass default
[08/24 03:39:16] [DEBUG] DataSourceTransactionManager: Triggering beforeCommit synchronization
[08/24 03:39:16] [DEBUG] DataSourceTransactionManager: Triggering beforeCompletion synchronization
[08/24 03:39:16] [DEBUG] DataSourceTransactionManager: Initiating transaction commit
[08/24 03:39:16] [DEBUG] DataSourceTransactionManager: Committing JDBC transaction on Connection [mons.dbcp.PoolableConnection@1db05b2]
[08/24 03:39:16] [DEBUG] DataSourceTransactionManager: Triggering afterCommit synchronization
[08/24 03:41:40] [DEBUG] TransactionInterceptor: Completing transaction for [usertest.dao.UsersDAO.testInsertAndUpdate] after exception: usertest.exception.BusinessException: Error
[08/24 03:41:40] [DEBUG] RuleBasedTransactionAttribute: Applying rules to determine whether transaction should rollback on usertest.exception.BusinessException: Error
[08/24 03:41:40] [DEBUG] RuleBasedTransactionAttribute: Winning rollback rule is: RollbackRuleAttribute with pattern [Exception]
[08/24 03:41:40] [DEBUG] DataSourceTransactionManager: Triggering beforeCompletion synchronization
[08/24 03:41:40] [DEBUG] DataSourceTransactionManager: Initiating transaction rollback
[08/24 03:41:40] [DEBUG] DataSourceTransactionManager: Rolling back JDBC transaction on Connection [mons.dbcp.PoolableConnection@1db05b2]
[08/24 03:41:40] [DEBUG] DataSourceTransactionManager: Triggering afterCompletion synchronization
[08/24 03:41:40] [DEBUG] TransactionSynchronizationManager: Clearing transaction synchronization
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:185895次
积分:2339
积分:2339
排名:第11349名
原创:51篇
转载:46篇
评论:11条
(1)(3)(2)(1)(1)(1)(1)(1)(1)(2)(3)(5)(2)(2)(1)(5)(7)(1)(2)(1)(2)(8)(15)(6)(1)(4)(3)(8)(7)
下载jar包,查看源码等
API介绍网站 /html

我要回帖

更多关于 php activerecord 的文章

 

随机推荐