Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yii2
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Rotua Panjaitan
yii2
Commits
a54c1cd6
Commit
a54c1cd6
authored
May 13, 2013
by
resurtm
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fixes #226: atomic operations and transaction support in AR.
parent
0f7f1936
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
221 additions
and
70 deletions
+221
-70
ActiveRecord.md
docs/api/db/ActiveRecord.md
+4
-0
ar.md
docs/internals/ar.md
+39
-4
Model.php
yii/base/Model.php
+23
-16
ActiveRecord.php
yii/db/ActiveRecord.php
+155
-50
No files found.
docs/api/db/ActiveRecord.md
View file @
a54c1cd6
...
@@ -446,3 +446,7 @@ $customers = Customer::find()->olderThan(50)->all();
...
@@ -446,3 +446,7 @@ $customers = Customer::find()->olderThan(50)->all();
The parameters should follow after the
`$query`
parameter when defining the scope method, and they
The parameters should follow after the
`$query`
parameter when defining the scope method, and they
can take default values like shown above.
can take default values like shown above.
### Atomic operations and scenarios
TBD
docs/internals/ar.md
View file @
a54c1cd6
ActiveRecord
ActiveRecord
============
============
Scenarios
---------
All possible scenario formats supported by ActiveRecord:
```
php
public
function
scenarios
()
{
return
array
(
// 1. attributes array
'scenario1'
=>
array
(
'attribute1'
,
'attribute2'
),
// 2. insert, update and delete operations won't be wrapped with transaction (default mode)
'scenario2'
=>
array
(
'attributes'
=>
array
(
'attribute1'
,
'attribute2'
),
'atomic'
=>
array
(),
// default value
),
// 3. all three operations (insert, update and delete) will be wrapped with transaction
'scenario3'
=>
array
(
'attributes'
=>
array
(
'attribute1'
,
'attribute2'
),
'atomic'
,
),
// 4. insert and update operations will be wrapped with transaction, delete won't
'scenario4'
=>
array
(
'attributes'
=>
array
(
'attribute1'
,
'attribute2'
),
'atomic'
=>
array
(
self
::
INSERT
,
self
::
UPDATE
),
),
// 5. insert and update operations won't be wrapped with transaction, delete will
'scenario5'
=>
array
(
'attributes'
=>
array
(
'attribute1'
,
'attribute2'
),
'atomic'
=>
array
(
self
::
DELETE
),
),
);
}
```
Query
Query
-----
-----
### Basic Queries
### Basic Queries
### Relational Queries
### Relational Queries
### Scopes
### Scopes
yii/base/Model.php
View file @
a54c1cd6
...
@@ -590,18 +590,22 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
...
@@ -590,18 +590,22 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
/**
/**
* Returns the attribute names that are safe to be massively assigned in the current scenario.
* Returns the attribute names that are safe to be massively assigned in the current scenario.
* @return
array
safe attribute names
* @return
string[]
safe attribute names
*/
*/
public
function
safeAttributes
()
public
function
safeAttributes
()
{
{
$scenario
=
$this
->
getScenario
();
$scenario
=
$this
->
getScenario
();
$scenarios
=
$this
->
scenarios
();
$scenarios
=
$this
->
scenarios
();
if
(
!
isset
(
$scenarios
[
$scenario
]))
{
return
array
();
}
$attributes
=
array
();
$attributes
=
array
();
if
(
isset
(
$scenarios
[
$scenario
]))
{
if
(
isset
(
$scenarios
[
$scenario
][
'attributes'
])
&&
is_array
(
$scenarios
[
$scenario
][
'attributes'
]))
{
foreach
(
$scenarios
[
$scenario
]
as
$attribute
)
{
$scenarios
[
$scenario
]
=
$scenarios
[
$scenario
][
'attributes'
];
if
(
$attribute
[
0
]
!==
'!'
)
{
}
$attributes
[]
=
$attribute
;
foreach
(
$scenarios
[
$scenario
]
as
$attribute
)
{
}
if
(
$attribute
[
0
]
!==
'!'
)
{
$attributes
[]
=
$attribute
;
}
}
}
}
return
$attributes
;
return
$attributes
;
...
@@ -609,23 +613,26 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
...
@@ -609,23 +613,26 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
/**
/**
* Returns the attribute names that are subject to validation in the current scenario.
* Returns the attribute names that are subject to validation in the current scenario.
* @return
array
safe attribute names
* @return
string[]
safe attribute names
*/
*/
public
function
activeAttributes
()
public
function
activeAttributes
()
{
{
$scenario
=
$this
->
getScenario
();
$scenario
=
$this
->
getScenario
();
$scenarios
=
$this
->
scenarios
();
$scenarios
=
$this
->
scenarios
();
if
(
isset
(
$scenarios
[
$scenario
]))
{
if
(
!
isset
(
$scenarios
[
$scenario
]))
{
$attributes
=
$scenarios
[
$this
->
getScenario
()];
foreach
(
$attributes
as
$i
=>
$attribute
)
{
if
(
$attribute
[
0
]
===
'!'
)
{
$attributes
[
$i
]
=
substr
(
$attribute
,
1
);
}
}
return
$attributes
;
}
else
{
return
array
();
return
array
();
}
}
if
(
isset
(
$scenarios
[
$scenario
][
'attributes'
])
&&
is_array
(
$scenarios
[
$scenario
][
'attributes'
]))
{
$attributes
=
$scenarios
[
$scenario
][
'attributes'
];
}
else
{
$attributes
=
$scenarios
[
$scenario
];
}
foreach
(
$attributes
as
$i
=>
$attribute
)
{
if
(
$attribute
[
0
]
===
'!'
)
{
$attributes
[
$i
]
=
substr
(
$attribute
,
1
);
}
}
return
$attributes
;
}
}
/**
/**
...
...
yii/db/ActiveRecord.php
View file @
a54c1cd6
...
@@ -74,6 +74,22 @@ class ActiveRecord extends Model
...
@@ -74,6 +74,22 @@ class ActiveRecord extends Model
const
EVENT_AFTER_DELETE
=
'afterDelete'
;
const
EVENT_AFTER_DELETE
=
'afterDelete'
;
/**
/**
* Represents insert ActiveRecord operation. This constant is used for specifying set of atomic operations
* for particular scenario in the [[scenarios()]] method.
*/
const
OPERATION_INSERT
=
'insert'
;
/**
* Represents update ActiveRecord operation. This constant is used for specifying set of atomic operations
* for particular scenario in the [[scenarios()]] method.
*/
const
OPERATION_UPDATE
=
'update'
;
/**
* Represents delete ActiveRecord operation. This constant is used for specifying set of atomic operations
* for particular scenario in the [[scenarios()]] method.
*/
const
OPERATION_DELETE
=
'delete'
;
/**
* @var array attribute values indexed by attribute names
* @var array attribute values indexed by attribute names
*/
*/
private
$_attributes
=
array
();
private
$_attributes
=
array
();
...
@@ -664,10 +680,39 @@ class ActiveRecord extends Model
...
@@ -664,10 +680,39 @@ class ActiveRecord extends Model
* @param array $attributes list of attributes that need to be saved. Defaults to null,
* @param array $attributes list of attributes that need to be saved. Defaults to null,
* meaning all attributes that are loaded from DB will be saved.
* meaning all attributes that are loaded from DB will be saved.
* @return boolean whether the attributes are valid and the record is inserted successfully.
* @return boolean whether the attributes are valid and the record is inserted successfully.
* @throws \Exception in case insert failed.
*/
*/
public
function
insert
(
$runValidation
=
true
,
$attributes
=
null
)
public
function
insert
(
$runValidation
=
true
,
$attributes
=
null
)
{
{
if
(
$runValidation
&&
!
$this
->
validate
(
$attributes
)
||
!
$this
->
beforeSave
(
true
))
{
if
(
$runValidation
&&
!
$this
->
validate
(
$attributes
))
{
return
false
;
}
$db
=
static
::
getDb
();
$transaction
=
$this
->
isOperationAtomic
(
self
::
OPERATION_INSERT
)
&&
null
===
$db
->
getTransaction
()
?
$db
->
beginTransaction
()
:
null
;
try
{
$result
=
$this
->
internalInsert
(
$attributes
);
if
(
null
!==
$transaction
)
{
if
(
false
===
$result
)
{
$transaction
->
rollback
();
}
else
{
$transaction
->
commit
();
}
}
}
catch
(
\Exception
$e
)
{
if
(
null
!==
$transaction
)
{
$transaction
->
rollback
();
}
throw
$e
;
}
return
$result
;
}
/**
* @see ActiveRecord::insert()
*/
private
function
internalInsert
(
$attributes
=
null
)
{
if
(
!
$this
->
beforeSave
(
true
))
{
return
false
;
return
false
;
}
}
$values
=
$this
->
getDirtyAttributes
(
$attributes
);
$values
=
$this
->
getDirtyAttributes
(
$attributes
);
...
@@ -678,22 +723,23 @@ class ActiveRecord extends Model
...
@@ -678,22 +723,23 @@ class ActiveRecord extends Model
}
}
$db
=
static
::
getDb
();
$db
=
static
::
getDb
();
$command
=
$db
->
createCommand
()
->
insert
(
$this
->
tableName
(),
$values
);
$command
=
$db
->
createCommand
()
->
insert
(
$this
->
tableName
(),
$values
);
if
(
$command
->
execute
())
{
if
(
!
$command
->
execute
())
{
$table
=
$this
->
getTableSchema
();
return
false
;
if
(
$table
->
sequenceName
!==
null
)
{
}
foreach
(
$table
->
primaryKey
as
$name
)
{
$table
=
$this
->
getTableSchema
();
if
(
!
isset
(
$this
->
_attributes
[
$name
]))
{
if
(
$table
->
sequenceName
!==
null
)
{
$this
->
_oldAttributes
[
$name
]
=
$this
->
_attributes
[
$name
]
=
$db
->
getLastInsertID
(
$table
->
sequenceName
);
foreach
(
$table
->
primaryKey
as
$name
)
{
break
;
if
(
!
isset
(
$this
->
_attributes
[
$name
]))
{
}
$this
->
_oldAttributes
[
$name
]
=
$this
->
_attributes
[
$name
]
=
$db
->
getLastInsertID
(
$table
->
sequenceName
);
break
;
}
}
}
}
foreach
(
$values
as
$name
=>
$value
)
{
$this
->
_oldAttributes
[
$name
]
=
$value
;
}
$this
->
afterSave
(
true
);
return
true
;
}
}
foreach
(
$values
as
$name
=>
$value
)
{
$this
->
_oldAttributes
[
$name
]
=
$value
;
}
$this
->
afterSave
(
true
);
return
true
;
}
}
/**
/**
...
@@ -744,39 +790,67 @@ class ActiveRecord extends Model
...
@@ -744,39 +790,67 @@ class ActiveRecord extends Model
* or [[beforeSave()]] stops the updating process.
* or [[beforeSave()]] stops the updating process.
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
* being updated is outdated.
* being updated is outdated.
* @throws \Exception in case update failed.
*/
*/
public
function
update
(
$runValidation
=
true
,
$attributes
=
null
)
public
function
update
(
$runValidation
=
true
,
$attributes
=
null
)
{
{
if
(
$runValidation
&&
!
$this
->
validate
(
$attributes
)
||
!
$this
->
beforeSave
(
false
)
)
{
if
(
$runValidation
&&
!
$this
->
validate
(
$attributes
))
{
return
false
;
return
false
;
}
}
$values
=
$this
->
getDirtyAttributes
(
$attributes
);
$db
=
static
::
getDb
();
if
(
!
empty
(
$values
))
{
$transaction
=
$this
->
isOperationAtomic
(
self
::
OPERATION_UPDATE
)
&&
null
===
$db
->
getTransaction
()
?
$db
->
beginTransaction
()
:
null
;
$condition
=
$this
->
getOldPrimaryKey
(
true
);
try
{
$lock
=
$this
->
optimisticLock
();
$result
=
$this
->
internalUpdate
(
$attributes
);
if
(
$lock
!==
null
)
{
if
(
null
!==
$transaction
)
{
if
(
!
isset
(
$values
[
$lock
]))
{
if
(
false
===
$result
)
{
$values
[
$lock
]
=
$this
->
$lock
+
1
;
$transaction
->
rollback
();
}
else
{
$transaction
->
commit
();
}
}
$condition
[
$lock
]
=
$this
->
$lock
;
}
}
// We do not check the return value of updateAll() because it's possible
}
catch
(
\Exception
$e
)
{
// that the UPDATE statement doesn't change anything and thus returns 0.
if
(
null
!==
$transaction
)
{
$rows
=
$this
->
updateAll
(
$values
,
$condition
);
$transaction
->
rollback
();
if
(
$lock
!==
null
&&
!
$rows
)
{
throw
new
StaleObjectException
(
'The object being updated is outdated.'
);
}
}
throw
$e
;
}
return
$result
;
}
foreach
(
$values
as
$name
=>
$value
)
{
/**
$this
->
_oldAttributes
[
$name
]
=
$this
->
_attributes
[
$name
];
* @see CActiveRecord::update()
* @throws StaleObjectException
*/
private
function
internalUpdate
(
$attributes
=
null
)
{
if
(
!
$this
->
beforeSave
(
false
))
{
return
false
;
}
$values
=
$this
->
getDirtyAttributes
(
$attributes
);
if
(
empty
(
$values
))
{
return
0
;
}
$condition
=
$this
->
getOldPrimaryKey
(
true
);
$lock
=
$this
->
optimisticLock
();
if
(
$lock
!==
null
)
{
if
(
!
isset
(
$values
[
$lock
]))
{
$values
[
$lock
]
=
$this
->
$lock
+
1
;
}
}
$condition
[
$lock
]
=
$this
->
$lock
;
}
// We do not check the return value of updateAll() because it's possible
// that the UPDATE statement doesn't change anything and thus returns 0.
$rows
=
$this
->
updateAll
(
$values
,
$condition
);
$this
->
afterSave
(
false
);
if
(
$lock
!==
null
&&
!
$rows
)
{
return
$rows
;
throw
new
StaleObjectException
(
'The object being updated is outdated.'
);
}
else
{
}
return
0
;
foreach
(
$values
as
$name
=>
$value
)
{
$this
->
_oldAttributes
[
$name
]
=
$this
->
_attributes
[
$name
];
}
}
$this
->
afterSave
(
false
);
return
$rows
;
}
}
/**
/**
...
@@ -826,27 +900,43 @@ class ActiveRecord extends Model
...
@@ -826,27 +900,43 @@ class ActiveRecord extends Model
* Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
* Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
* being deleted is outdated.
* being deleted is outdated.
* @throws \Exception in case delete failed.
*/
*/
public
function
delete
()
public
function
delete
()
{
{
if
(
$this
->
beforeDelete
())
{
$db
=
static
::
getDb
();
// we do not check the return value of deleteAll() because it's possible
$transaction
=
$this
->
isOperationAtomic
(
self
::
OPERATION_DELETE
)
&&
null
===
$db
->
getTransaction
()
?
$db
->
beginTransaction
()
:
null
;
// the record is already deleted in the database and thus the method will return 0
try
{
$condition
=
$this
->
getOldPrimaryKey
(
true
);
$result
=
false
;
$lock
=
$this
->
optimisticLock
();
if
(
$this
->
beforeDelete
())
{
if
(
$lock
!==
null
)
{
// we do not check the return value of deleteAll() because it's possible
$condition
[
$lock
]
=
$this
->
$lock
;
// the record is already deleted in the database and thus the method will return 0
$condition
=
$this
->
getOldPrimaryKey
(
true
);
$lock
=
$this
->
optimisticLock
();
if
(
null
!==
$lock
)
{
$condition
[
$lock
]
=
$this
->
$lock
;
}
$result
=
$this
->
deleteAll
(
$condition
);
if
(
null
!==
$lock
&&
!
$result
)
{
throw
new
StaleObjectException
(
'The object being deleted is outdated.'
);
}
$this
->
_oldAttributes
=
null
;
$this
->
afterDelete
();
}
}
$rows
=
$this
->
deleteAll
(
$condition
);
if
(
null
!==
$transaction
)
{
if
(
$lock
!==
null
&&
!
$rows
)
{
if
(
false
===
$result
)
{
throw
new
StaleObjectException
(
'The object being deleted is outdated.'
);
$transaction
->
rollback
();
}
else
{
$transaction
->
commit
();
}
}
}
$this
->
_oldAttributes
=
null
;
}
catch
(
\Exception
$e
)
{
$this
->
afterDelete
();
if
(
null
!==
$transaction
)
{
return
$rows
;
$transaction
->
rollback
()
;
}
else
{
}
return
fals
e
;
throw
$
e
;
}
}
return
$result
;
}
}
/**
/**
...
@@ -1336,4 +1426,19 @@ class ActiveRecord extends Model
...
@@ -1336,4 +1426,19 @@ class ActiveRecord extends Model
}
}
return
true
;
return
true
;
}
}
/**
* @param string $operation possible values are ActiveRecord::INSERT, ActiveRecord::UPDATE and ActiveRecord::DELETE.
* @return boolean whether given operation is atomic. Currently active scenario is taken into account.
*/
private
function
isOperationAtomic
(
$operation
)
{
$scenario
=
$this
->
getScenario
();
$scenarios
=
$this
->
scenarios
();
if
(
!
isset
(
$scenarios
[
$scenario
])
||
!
isset
(
$scenarios
[
$scenario
][
'attributes'
])
||
!
is_array
(
$scenarios
[
$scenario
][
'attributes'
]))
{
return
false
;
}
return
in_array
(
'atomic'
,
$scenarios
[
$scenario
])
||
isset
(
$scenarios
[
$scenario
][
'atomic'
])
&&
is_array
(
$scenarios
[
$scenario
][
'atomic'
])
&&
in_array
(
$operation
,
$scenarios
[
$scenario
][
'atomic'
]);
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment