is there a way to hack codeigniter to let it by default when doing any insert/update inserts that timespan and user_id into a specific columns in table ?
i have 8 tables in database, all contain (update_time,updated_by)columns , i want to set codeigniter active records so that when ever and update/insert is occuring it automaticly populate update_time=time() and update_by=$this->session->userdata('user')->id;
also on same concept i want to hack delete action into updating column(archived)=1 instead of deleting (since my mysql user doesnt have delete record access anyway)
is it possible ?
If you want to hack CI to accomplish this you should look in the system/drivers/DB_Active_rec.php (given that you use the active record functionality in CI)
For example, you can insert the current time on an update by modifying the update method in that file. Change this:
public function update($table = '', $set = NULL, $where = NULL, $limit = NULL)
{
// Combine any cached components with the current statements
$this->_merge_cache();
if ( ! is_null($set))
{
$this->set($set);
}
[...]
into:
public function update($table = '', $set = NULL, $where = NULL, $limit = NULL)
{
// Combine any cached components with the current statements
$this->_merge_cache();
// start of Insert updated_time hack
$time_format = 'Y-m-d H:i:s';
$set['update_time'] = date($time_format);
// end of hack
if ( ! is_null($set))
{
$this->set($set);
}
[...]
You can use the existing functionality of mysql to auto update the update_time column when a record is inserted or updated. However, updating the user_id is something you would have to do manually from your application.
Your other requirement is generally referred to as a soft delete by some, where you mark a row as deleted by turning a flag on a column. This is possible, but you have to make sure that all your select and update queries are avoiding rows with this flag turned on.
It is best that you carefully construct your Model methods to adhere to these two requirements of the application. You can perhaps maks use of extending the core Model of CI to have a customised update / select queries that will be used by all child classes that extends MY_Model.
Related
What I am trying to do
I want to query a specific set of records using active model like so
$jobModel = Jobs::find()->select('JOB_CODE')->distinct()->where(['DEPT_ID'=>$dept_id])->all();
Then I want to assign a flag attribute to the records in this activerecord based on whether they appear in a relationship table
What I have tried
So in my job model, I have declared a new attribute inAccount. Then I added this function in the job model that sets the inAccount flag to -1 or 0 based on whether a record is found in the relationship table with the specified account_id
public function assignInAccount($account_id){
if(JobCodeAccounts::find()->where(['JOB_CODE'=>$this->JOB_CODE])->andWhere(['ACCOUNT_ID'=>$account_id])->one() == null){
$this->inAccount=0;
}
else{
$this->inAccount = -1;
}
}
What I have been doing is assigning each value individually using foreach like so
foreach($jobModel as $job){
$job->assignInAccount($account_id);
}
However, this is obviously very slow because if I have a large number of records in $jobModel, and each one makes a db query in assignInAccount() this could obviously take some time if the db is slow.
What I am looking for
I am wondering if there is a more efficient way to do this, so that I can assign inAccount to all job records at once. I considered using afterFind() but I don't think this would work as I need to specify a specific parameter. I am wondering if there is a way I can pass in an entire model (or at least array of models/model-attributes and then do all the assignations running only a single query.
I should mention that I do need to end up with the original $jobModel activerecord as well
Thanks to scaisEdge's answer I was able to come up with an alternative solution, first finding the array of jobs that need to be flagged like so:
$inAccountJobs = array_column(Yii::$app->db->createCommand('Select * from job_code_accounts where ACCOUNT_ID = :account_id')
->bindValues([':account_id' => $account_id])->queryAll(), 'JOB_CODE');
and then checking each job record to see if it appears in this array like so
foreach($jobModel as $job){
if(in_array($job->JOB_CODE, $inAccountJobs))
$job->inAccount = -1;
else
$job->inAccount = 0;
}
Does seem to be noticeably faster as it requires only a single query.
I am using PHP Yii framework's Active Records to model a relation between two tables. The join involves a column and a literal, and could match 2+ rows but must be limited to only ever return 1 row.
I'm using Yii version 1.1.13, and MySQL 5.1.something.
My problem isn't the SQL, but how to configure the Yii model classes to work in all cases. I can get the classes to work sometimes (simple eager loading) but not always (never for lazy loading).
First I will describe the database. Then the goal. Then I will include examples of code I've tried and why it failed.
Sorry for the length, this is complex and examples are necessary.
The database:
TABLE sites
columns:
id INT
name VARCHAR
type VARCHAR
rows:
id name type
-- ------- -----
1 Site A foo
2 Site B bar
3 Site C bar
TABLE field_options
columns:
id INT
field VARCHAR
option_value VARCHAR
option_label VARCHAR
rows:
id field option_value option_label
-- ----------- ------------- -------------
1 sites.type foo Foo Style Site
2 sites.type bar Bar-Like Site
3 sites.type bar Bar Site
So sites has an informal a reference to field_options where:
field_options.field = 'sites.type' and
field_options.option_value = sites.type
The goal:
The goal is for sites to look up the relevant field_options.option_label to go with its type value. If there happens to be more than one matching row, pick only one (any one, doesn't matter which).
Using SQL this is easy, I can do it 2 ways:
I can join using a subquery:
SELECT
sites.id,
f1.option_label AS type_label
FROM sites
LEFT JOIN field_options AS f1 ON f1.id = (
SELECT id FROM field_options
WHERE
field_options.field = 'sites.type'
AND field_options.option_value = sites.type
LIMIT 1
)
Or I can use a subquery as a column reference in the select clause:
SELECT
sites.id,
(
SELECT id FROM field_options
WHERE
field_options.field = 'sites.type'
AND field_options.option_value = sites.type
LIMIT 1
) AS type_label
FROM sites
Either way works great. So how do I model this in Yii??
What I've tried so far:
1. Use "on" array key in relation
I can get a simple eager lookup to work with this code:
class Sites extends CActiveRecord
{
...
public function relations()
{
return array(
'type_option' => array(
self::BELONGS_TO,
'FieldOptions', // that's the class for field_options
'', // no normal foreign key
'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = t.type LIMIT 1)",
),
);
}
}
This works when I load a set of Sites objects and force it to eager load type_label, e.g. Sites::model()->with('type_label')->findByPk(1).
It does not work if type_label is lazy-loaded.
$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // ERROR: column t.type doesn't exist
2. Force eager loading always
Building on #1 above, I tried forcing Yii to always to eager loading, never lazy loading:
class Sites extends CActiveRecord
{
public function relations()
{
....
}
public function defaultScope()
{
return array(
'with' => array( 'type_option' ),
);
}
}
Now everything always works when I load Sites, but it's no good because there are other models (not pictured here) that have relations that point to Sites, and those result in errors:
$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // works now
$other = OtherModel::model()->with('site_relation')->findByPk(1); // ERROR: column t.type doesn't exist, because 't' refers to OtherModel now
3. Make the reference to the base table somehow relative
If there was a way that I could refer to the base table, other than "t", that was guaranteed to point to the correct alias, that would work, e.g.
'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = %%BASE_TABLE%%.type LIMIT 1)",
where %%BASE_TABLE%% always refers to the correct alias for table sites. But I know of no such token.
4. Add a true virtual database column
This way would be the best, if I could convince Yii that the table has an extra column, which should be loaded just like every other column, except the SQL is a subquery -- that would be awesome. But again, I don't see any way to mess with the column list, it's all done automatically.
So, after all that... does anyone have any ideas?
EDIT Mar 21/15: I just spent a long time investigating the possibility of subclassing parts of Yii to get the job done. No luck.
I tried creating a new type of relation based on BELONGS_TO (class CBelongsToRelation), to see if I could somehow add in context sensitivity so it could react differently depending on whether it was being lazy-loaded or not. But Yii isn't built that way. There is no place where I can hook in code during query buiding from inside a relation object. And there is also no way I can tell even what the base class is, relation objects have no link back to the parent model.
All of the code that assembles these queries for active records and their relations is locked up in a separate set of classes (CActiveFinder, CJoinQuery, etc.) that cannot be extended or replaced without replacing the entire AR system pretty much. So that's out.
I then tried to see if I can create "fake" database column entries that would actually be a subquery. Answer: no. I figured out how I could add additional columns to Yii's automatically generated schema data. But,
a) there's no way to define a column in such a way that it can be a derived value, Yii assumes it's a column name in way too many places for that; and
b) there also doesn't appear to be any way to avoid having it try to insert/update to those columns on save.
So it really is looking like Yii (1.x) just does not have any way to make this happen.
Limited solution provided by #eggyal in comments: #eggyal has a suggestion that will meet my needs. He suggests creating a MySQL view table to add extra columns for each label, using a subquery to look up the value. To allow editing, the view would have to be tied to a separate Yii class, so the downside is everywhere in my code I need to be aware of whether I'm loading a record for reading only (must use the view's class) or read/write (must use the base table's class, does not have the extra columns). That said, it is a workable solution for my particular case, maybe even the only solution -- although not an answer to this question as written, so I'm not going to put it in as an answer.
OK, after a lot of attempts, I have found a solution. Thanks to #eggyal for making me think about database views.
As a quick recap, my goal was:
link one Yii model (CActiveRecord) to another using a relation()
the table join is complex and could match more than one row
the relation must never join more than one row (i.e. LIMIT 1)
I got it to work by:
creating a view from the field_options base table, using SQL GROUP BY to eliminate duplicate rows
creating a separate Yii model (CActiveRecord class) for the view
using the new model/view for the relation(), not the original table
Even then there were some wrinkles (maybe a Yii bug?) I had to work around.
Here are all the details:
The SQL view:
CREATE VIEW field_options_distinct AS
SELECT
field,
option_value,
option_label
FROM
field_options
GROUP BY
field,
option_value
;
This view contains only the columns I care about, and only ever one row per field/option_value pair.
The Yii model class:
class FieldOptionsDistinct extends CActiveRecord
{
public function tableName()
{
return 'field_options_distinct'; // the view
}
/*
I found I needed the following to override Yii's default table data.
The view doesn't have a primary key, and that confused Yii's AR finding system
and resulted in a PHP "invalid foreach()" error.
So the code below works around it by diving into the Yii table metadata object
and manually setting the primary key column list.
*/
private $bMetaDataSet = FALSE;
public function getMetaData()
{
$oMetaData = parent::getMetaData();
if (!$this->bMetaDataSet) {
$oMetaData->tableSchema->primaryKey = array( 'field', 'option_value' );
$this->bMetaDataSet = TRUE;
}
return $oMetaData;
}
}
The Yii relation():
class Sites extends CActiveRecord
{
// ...
public function relations()
{
return (
'type_option' => array(
self::BELONGS_TO,
'FieldOptionsDistinct',
array(
'type' => 'option_value',
),
'on' => "type_option.field = 'sites.type'",
),
);
}
}
And all that does the trick. Easy, right?!?
I have model A and model B which lie in two different databases.
Now I have a pivot_table called a_bs in the same database as model A.
I've setup the belongsToMany relatinoship like this in model A
public function bs()
{
return $this->belongsToMany('B', 'a_bs', 'a_id', 'b_id');
}
When I try to access this relationship like so:
$a = A::find($id);
print_r($a->bs->lists('id'));
I get an error that my pivot table doesn't exist in model B's database. Which is obviously correct since the pivot table is in model A's database. How can I let Laravel know that?
Do not suggest to put the pivot table in model B's database
Very simply:
public function bs()
{
$database = $this->getConnection()->getDatabaseName();
return $this->belongsToMany('B', "$database.a_bs", 'a_id', 'b_id');
}
I'm obtaining the database name dynamically because my connection is configured based off an environment variable. Laravel seems to assume the pivot table to exist in the same database as the target relation, so this will force it to look instead to the database corresponding to the model that this method is in, your 'A' realm.
If you're not worried about SQLite databases, i.e. in the scope of a unit-test, that's all you need. But if you are, keep reading.
Firstly, the previous example isn't sufficient on its own. The value of $database would end up being a file-path, so you need to alias it to something that won't break an SQL statement, and make it accessible to the current connection. "ATTACH DATABASE '$database' AS $name" is how you do that:
public function bs()
{
$database = $this->getConnection()->getDatabaseName();
if (is_file($database)) {
$connection = app('B')->getConnection()->getName();
$name = $this->getConnection()->getName();
\Illuminate\Support\Facades\DB::connection($connection)->statement("ATTACH DATABASE '$database' AS $name");
$database = $name;
}
return $this->belongsToMany('B', "$database.a_bs", 'a_id', 'b_id');
}
Warning: Transactions muck this up: If the current connection is using transactions, the ATTACH DATABASE statement will fail. You can use transactions on it after executing that statement though.
Whereas, if the related connection uses transactions, the resulting data will be silently rendered invisible to the current one. This drove me nuts for longer than I'd care to admit, because my queries ran without error, but kept coming up empty. It seems only data truly written to the attached database is actually accessible to the one it's attached to.
So, after being forced to write to your attached database, you may still want your test to clean up after itself. A simple solution there would be to just use $this->artisan('migrate:rollback', ['--database' => $attachedConnectionName]);. But if you have multiple tests that need the same tables, this is not very efficient, as it forces them to have to rebuild them each time.
A better option would be to truncate the tables, but leave their structure in tact:
//Get all tables within the attached database
collect(DB::connection($database)->select("SELECT name FROM sqlite_master WHERE type = 'table'"))->each(function ($table) use ($name) {
//Clear all entries for the table
DB::connection($database)->delete("DELETE FROM '$table->name'");
//Reset any auto-incremented index value
DB::connection($database)->delete("DELETE FROM sqlite_sequence WHERE name = '$table->name'");
});
}
This will wipe all data from that connection, but there's no reason you couldn't apply some kind filter to that however you see fit. Alternatively, you could take advantage of the fact that SQLite DBs are easily-accessible files, and just copy the attached one to a temp file, and use it to overwrite the source after the test is done executing. The result would be functionally identical to a transaction.
You can set the database of the table in the model class:
protected $table = 'A.a_s';
And You have to use singular form when create a pivot table.
/app/model/A.php
class A extends Eloquent {
// Set table name (plural) with database name
protected $table = 'A.a_s';
// Many to many relation
public function b_s() {
return $this->belongsToMany('B');
}
}
/app/model/B.php
class B extends Eloquent {
// Set table name (plural) with database name
protected $table = 'B.b_s';
}
Query
print_r(A::with('b_s')->where('id', 1)->get()->toArray());
MySQL
CREATE TABLE `A`.`a_s` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);
CREATE TABLE `B`.`b_s` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);
CREATE TABLE `A`.`a_b` (
`a_id` int(10) unsigned NOT NULL,
`b_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`a_id`,`b_id`)
) ENGINE=InnoDB;
INSERT INTO A.a_s VALUES (NULL);
INSERT INTO A.a_s VALUES (NULL);
INSERT INTO B.b_s VALUES (NULL);
INSERT INTO A.a_b VALUES (1,1);
INSERT INTO A.a_b VALUES (1,2);
If the databases/schemas are on the same host server like #NiRR said just do this to override the default schema of the second connection:
return $this->belongsToMany('B', 'real-schema-name.a_bs');
or alternatively
return $this->belongsToMany('A', 'real-schema-name.a_bs');
Depending on which model (A or B) is defined with the connection that isn't using the default schema.
Remember that it's impossible to do a join query that spans across two servers; which server is it going to get executed on? Each is missing some of the needed data needed to preform the request.
This is only possible if the two databases are on the same connection (server).
Its not possible to do this on two different connections (servers) since you'll need all three tables on a single server that will perform the join command.
Explicitly setting or defining the connection for both models works just fine.
protected $connection = 'connection1'; //inside model A
protected $connection = 'connection2'; //inside model B
EDIT
When Laravel is fetching a model from the DB, provided a database/connection has been defined (as a property) within the model, Laravel will use the database name for that connection when constructing the SQL. So when working with multiple connections, it is best to define the connection for every model.
How can I get the number of affected rows after create, update or delete using ORM for Kohana? I checked the main class but don't return that value...
If there any possible solution to do this, please let me know. If it is possible not use the Query Builder, i prefer use only the ORM class.
Thank you.
Due to its Cascading Filesystem it's very easy in Kohana to make some changes in system or module classes.
In your case you need to redefine methods of ORM module main class. Its file is located in /modules/orm/classes/kohana/orm.php.
Create new ORM module main class file in /application/classes/orm.php and redefine create, update and delete methods so these methods to return the number of affected rows.
For example new update method should end with this:
// Update a single record
$update_result = DB::update($this->_table_name)
->set($data)
->where($this->_primary_key, '=', $id)
->execute($this->_db);
if (isset($data[$this->_primary_key]))
{
// Primary key was changed, reflect it
$this->_primary_key_value = $data[$this->_primary_key];
}
// Object has been saved
$this->_saved = TRUE;
// All changes have been saved
$this->_changed = array();
$this->_original_values = $this->_object;
return $update_result;
I'm building a PHP application using the data mapper pattern to separate my DB from the domain objects. I have a mapper class that returns Site objects based on data from the DB and accepts existing Site objects to be saved back to the DB.
My problem is that in the system one (and only one) of all the sites has to be marked as the "primary" site, which means that if I set one as the primary, I'd like to be able to automatically unset the current primary.
So, something like:
$mapper = new Site_Mapper();
$site = $mapper->fetch(2);
$site->isPrimary = true;
$mapper->save($site);
Would somehow in the background automatically do this:
$mapper = new Site_Mapper();
$site = $mapper->fetch(1);
$site->isPrimary = false;
$mapper->save($site);
Question is, where should the logic for automatically updating the existing primary site go? It has to happen after the object is saved back to the DB, not before, in case the DB query fails and you're left with no site as the primary.
Cheers,
Jack
Personally I would think that it makes the most sense to place the additional update logic with the Site_Mapper class, especially considering that you're dealing with the same table / mapper in both instances. You could simply override the save($siteObj) method so that it works like this:
public function save($siteObj)
{
// Save the passed object.
$sql = "UPDATE site SET isPrimary = 1 WHERE id != ?";
$stmt = new PDO_Statement($sql);
$stmt->execute($siteObj->id);
}
Obviously you could create either a custom save() function to do this perhaps a little more smoothly or you could use an if comparison to ensure that you actually need to run the update statement.
Sounds like a job for a database trigger.
DELIMITER $$
CREATE TRIGGER test_trigger AFTER INSERT ON table
FOR EACH ROW BEGIN
IF NEW.isPrimary = 1 THEN
UPDATE table
SET isPrimary = 0
WHERE id <> NEW.id;
END IF;
END$$
DELIMITER ;