This is a difficult question to ask as the code I have is working fine. Im just learning the YII platform and my issue isnt that I cant get what I want working, but moreso if there is a better way of doing this that takes advantage of the YII platform and its classes.
Basically I have a webstore using a platform called Lightspeed which uses the YII platform.
In the product detail section I am looking to pull its related products. Thankfully Lightspeed has the tables in place for this already (which gives me more reason to think I am doing this wrong).
Right now what Im doing seems a little hard coded.
In my view I have this to get the products...
$related_products = Product::GetRelatedProducts();
I have nothing in my controller, and in my model I have this..
public function getRelatedProducts()
{
$rawData=Yii::app()->db->createCommand('SELECT * FROM xlsws_product as Product LEFT JOIN xlsws_product_related as ProductRelated ON ProductRelated.related_id=Product.id WHERE ProductRelated.related_id=Product.id ')->queryAll();
return $rawData;
}
As I said there is nothing wrong with this code, but I see so much functionality in place with all the other queries in the model that it makes me think Im doing this incorrectly.
Examples include..
protected function getSliderCriteria($autoadd=0)
{
$criteria = new CDbCriteria();
$criteria->distinct = true;
$criteria->alias = 'Product';
$criteria->join='LEFT JOIN '.ProductRelated::model()->tableName().' as ProductRelated ON ProductRelated.related_id=Product.id';
if (_xls_get_conf('INVENTORY_OUT_ALLOW_ADD',0)==Product::InventoryMakeDisappear)
$criteria->condition = 'ProductRelated.product_id=:id AND inventory_avail>0 AND web=1 AND autoadd='.$autoadd.' AND parent IS NULL';
else
$criteria->condition = 'ProductRelated.product_id=:id AND web=1 AND autoadd='.$autoadd.' AND parent IS NULL';
$criteria->params = array(':id'=>$this->id);
$criteria->limit = _xls_get_conf('MAX_PRODUCTS_IN_SLIDER',64);
$criteria->order = 'Product.id DESC';
return $criteria;
}
Thats just an example of a widget that seems to use this data (although Im unsure how that data turns into arrays, as when I print out $criteria I get arrays containing query commands.
Let me know if you need more clarification on what Im looking for.
You're right that you're not leveraging Yii. Yii (and other MVC frameworks), abstract the database layer out into a model.
The getSliderCriteria() you show above is an example of building criteria to refine the interactions with the model.
What you should try and figure out is what model represents the data you're looking for in Lightspeed, then building and applying the criteria to it.
In most modern frameworks, you shouldn't be writing much (if any) raw SQL . . .
Related
I have a simple DB with multiple tables and relationships, ie:
Article - Category
User - Group
etc...
I have implemented SoftDelete behavior where there is a Active column and if set to 0, it is considered deleted.
My question is simple.
How to i specify in as few places as possible that i only want load Articles that belong to Active categories.
I have specified relationships and default scopes (with Active = 1) condition.
However, when i do findAll(), it returns those Articles that have Active = 1, even if the category it belongs to is Active = 0....
Thank you
Implementation so far:
In base class
public function defaultScope()
{
return array('condition' => 'Active = 1');
}
in model:
'category' => array(self::BELONGS_TO, 'Category', 'CategoryID'),
'query':
$data = Article::model()->findAll();
MY SOLUTION
So i decided, that doing it in framework is:
inneficient
too much work
not good as it moves business logic away from database - this is fairly important to save work later on when working on interfaces/webservices and other customizations that should be part of the product.
Overall lesson: Try to keep all business logic as close to database as possible to prevent disrepancies.
First, i was thinking using triggers that would propagate soft delete down the hierarchy. However after thinking a bit more i decided not to do this. The reason is, that this way if I (or an interface or something) decided to reactivate the parent records, there would be no way to say which child record was chain-deleted and which one was deleted before:
CASE:
Lets say Category and Article.
First, one article is deleted.
Then the whole category is deleted.
Then you realize this was a mistake and you want to undelete the Category. How do you know which article was deleted by deleting category and which one should stay deleted? Yes there are solutions, ie timestamps but ...... too complex, too easy to break
So my solution in the end are:
VIEWS. I think i will move away from yii ORM to using views for anything more complex then basic things.
There are two advantages to this for me:
1) as a DBA i can do better SQL faster
2) logic stays in database, in case the application changes/another one is added, there is no need to implement the logic in more then one places
You need to specify condition when you are using findAll method. So You should use CDbCriteria for this purpose:
$criteria=new CDbCriteria;
$criteria->with = "category";
$criteria->condition = "category.Active = 1"; //OR $criteria->compare('category.active', 1 true);
$data = Article::model()->findAll($criteria);
You should also have a defaultScope in your Article model, condition there should add category.Active = 1 or whatever your relation is named.
public function defaultScope()
{
return array('condition' => 't.Active = 1 AND category.Active = 1');
}
I don't remember by now but it might be you have to specify the relation:
return array(
'with' => array("category" => array(
'condition'=> "t.Active = 1 AND category.Active = 1",
)
);
I am looking to pull data for related products correctly with the YII framework, as Im only learning this framework Im wondering have you advice on how I should go about this.
Ive taken over a project which uses YII, and it seems to have already some functionality in place, but I dont know how to utilize it.
Here is my current code...
In my Product model I have written this myself..
public function getRelatedProducts($id)
{
$rawData=Yii::app()->db->createCommand('SELECT * FROM '.Product::model()->tableName().' as Product LEFT JOIN '.ProductRelated::model()->tableName().' as ProductRelated ON ProductRelated.related_id=Product.id LEFT JOIN '.Images::model()->tableName().' as image ON Product.image_id=image.id WHERE ProductRelated.product_id='.$id.' ')->queryAll();
return $rawData;
}
and I get this data using...
$related_products = Product::GetRelatedProducts($model->id);
This works but is not using the YII framework.
I have noticed there is a model called ProductRelated (with not much in it).But I am not sure how to use it.
This obviously refers to a table I have in the database called product_related, which has 2 fields, product_id and related_id, where related_id represents the id of the product to which it was assigned(related to).
I would like to use this class as it was obviously written with this in mind. Right now Im just skipping over it.
class ProductRelated extends BaseProductRelated {
public static function model($className=__CLASS__)
{
return parent::model($className);
}
public static function LoadByProductIdRelatedId($intProductId , $intRelatedId)
{
return ProductRelated::model()->findByAttributes(array('product_id'=>$intProductId,'related_id'=>$intRelatedId));
}
}
Having read up some more about YII I have rebuilt my query...
public function getRelatedProducts($id)
{
$criteria = new CDbCriteria();
$criteria->alias = 'Product';
$criteria->join ='LEFT JOIN '.ProductRelated::model()->tableName().' as ProductRelated ON ProductRelated.related_id=Product.id';
$criteria->join .=' LEFT JOIN '.Images::model()->tableName().' as image ON Product.image_id=image.id';
$criteria->condition = 'ProductRelated.product_id='.$id.'';
$criteria->order = 'Product.id DESC';
return $criteria;
}
However I am not sure what to do with that final bit of code and how to link it up with the model I have shown above.
there are many mistakes in your code. I don't think your predecessor known yii very well too.
There are included functionalities to get relations.
read about yii relational active records:
http://www.yiiframework.com/doc/guide/1.1/en/database.arr
Best regards
Yii model uses the ActiveRecord pattern. You'll find that the relations() method of your model allows you to define relational data.
If your Product.relations() method is properly set, then the related products shall be 'magically' available.
NB : I guess the relatedProduct table is an association table for a n-n relationship between Product and... Product so you will use MANY_MANY. Take a look at the sample code at the end of the documentation of the relations() method.
I have a code for defaultScope:
public function defaultScope()
{
$currentdb = explode('=', Yii::app()->db->connectionString);
return array(
'condition'=> "tenant=:tenant",
'params' => array(":tenant"=>$currentdb[2]));
}
And this code for Beforefind:
public function beforeFind() {
$currentdb = explode('=', Yii::app()->db->connectionString);
$criteria = new CDbCriteria;
$criteria->condition = "tenant=:tenant";
$criteria->params = array(":tenant"=>$currentdb[2]);
$this->dbCriteria->mergeWith($criteria);
parent::beforeFind();
}
I am getting same result in both the functions. Which function is better and why?
I think that both can accomplish what you want, but for me the best usage is using scopes. In the yii guide we can find the following definition for scopes:
A named scope represents a named query criteria that can be combined
with other named scopes and applied to an active record query.
It's is what you want to do: apply some query criteria before executing the query. And since you want those criteria to be added on every query then defaultScope is the way to go!
I disagree. I'm having a database with records for multiple users and I'm trying to filter on those records that should be visible for the current user only. I got stuck today on trying to fixing that with defaultScope and I found out that beforeFind is the way to go in this case. The problem can be nailed down to the fact that beforeFind doesn't seem to be used on the relations while defaultScope is. This means you get stuck when you apply criteria in your defaultScope of an object with relations that are eagerly loaded with alike criteria because of the order in which the criteria are applied in the joins.
Let me try to explain this with Yii's blog guide: when we only want the posts of the current author, we could write the following defaultScope:
$c = new CDbCriteria();
$c->with('author');
$c->addInCondition('author.author_id', array(1,2,3));
return $c;
When using $post->author, we will find out that author.author_id is applied before author is defined as a join. This is not the best example, but you will find yourself having these problems when having more than two joins in your relations.
Therefore, I would suggest using beforeFind instead of defaultScope.
OK lets say I want to select a number of columns from a database table, but I won't know what those columns are in the method. I could pass them in, but it could be more or less depending on the method calling the database method.
A quick fix would be SELECT *, but I understand that this is bad and can cause more data to be returned than is necessary, and I definitely don't need all the data from that table.
So I am using CodeIgniter and prepared statements to do this, and below is what I have currently (it works, just point that out).
function get_pages() {
$this->db->select('pages.id, pages.title, pages.on_nav, pages.date_added, admin.first_name, admin.last_name')
->from('pages, admin')
->where('pages.admin_id = admin.id')
->order_by('pages.id', 'ASC');
$query = $this->db->get();
return $query->result();
}
It's a simple function, but at the moment limited to getting only 'pages'. I want to convert this to work with getting from other tables too. What is the best way?
Many thanks in advance.
EDIT In CodeIgniter I have many Controllers. One for 'pages', one for 'products', one for 'news' and on and on. I don't want to create a single database query method in my model for each controller.
i think the desire to not have 4 methods is misguided. if you don't have the information in the method, you'll have to pass it in. so you could either pass in a string with the table you want and switch over that changing the query based on the table name, or pass in all of the necessary parts of the query. this would include table name, criteria column, criteria, and columns to select. and you'd need to pass that information in every time you called the function. neither of those two methods are really going to save you much code, and they're both less readable than a function for each purpose.
The entire idea with models to put your specific queries to the persistence layer in there. Using a generic catch-all method can be disastrous and hard to test. You should shape your model around the problem you're trying to solve.
This makes it much cleaner and easier to work with. At the same time you must also avoid the common trap of over-sizing models. Each model should follow the SRP. Try and separate concerns so that in your controller, you can easily see state changes.
Does that make sense or am I just rambling...?
In your model:
function get_pages($table_source) {
$this->db->select($table_source.".id"); // or $this->db->select('id');
// for instance, if one of your $table_source ="users" and there is no 'title' column you can write
if($table_source!='users') $this->db->select('title');
$this->db->select('on_nav');
$this->db->select('date_added');
$this->db->select('admin.first_name');
$this->db->select('admin.last_name');
$this->db->join('admin','admin.id = '.$table_source.'.admin_id')
$this->db->order_by('pages.id', 'ASC');
$query = $this->db->get($table_source);
return $query->result_array();
}
In your controller:
function all_tables_info() {
$tables = array("pages","users","customers");
$i=0;
foreach($tables as $table) {
$data[$i++]=$this->your_Model->get_pages($table);
}
//do somthing with $data
}
I have this query which I want to run in my PHP application back end. Notionally, sheet is a DB that keeps track of all the sheets we have. Purchases is a DB that keeps track of which users have access to which sheet. The query I want to run is given a user's id, I can get all the sheets that they should have access to. In query form:
select distinct s.wsid, s.name from sheets s, purchases p where
s.wsid = p.wsid AND p.uid = *value*;
where value is something input by the application
The way I see it there are two ways to go about getting this to work in the back end.
Option 1)
public function getPurchasedSheets($uid){
if( is_numeric($uid) ){ //check against injections
$query = "select distinct s.wsid, s.name from sheets s, purchases p
where s.wsid = p.wsid AND p.uid = ".$uid.";" ;
return $this->db->query($query);
} else {
return NULL; //or false not quite sure how typing works in PHP
}
}
Option 2)
public function getPurchasedSheets($uid){
if( is_numeric($uid) ){
$this->db->select('wsid, name');
$this->db->distinct();
$this->db->from('purchases');
//not sure which order the join works in...
$this->db->join('sheets', 'sheets.wsid = purchases.wsid');
$this->db->where('uid ='.$uid);
return $this->db->get();
} else {
return NULL;
}
}
Source for all the CodeIgniter Active Record commands:
codeigniter.com/user_guide/database/active_record.html
Is there some sort of performance or security difference from doing thing one way or another? Doing it the second way seems so much more confusing to me... This is compounded a little bit because I am not sure how to do referential disambiguation in this style of coding because purchases and sheets both have a uid field but they mean different things (in addition to not being very familiar with the SQL join command in the first place.). Uid (user id) in purchases means that user has purchased that sheet, while Uid in sheets denotes which user owns that sheet.
TL,DR: Basically, I am asking is there a reason I should sink time into looking how to do things the option 2 way?
The main benefits are:
Abstraction from the database engine, where the library can take care
of database-specific SQL syntax differences for you. Relevant if you
ever have/want to change the database you're working with. In theory,
the second form should still just work.
The 'Active Record' syntax
automatically escapes parameters for you.
Readability, although
that's a matter of taste.
Incidentally, if you're in a PHP 5 environment, the library supports method chaining:
if( is_numeric($uid) ){
return $this->db->select('wsid, name')
->distinct()
->from('purchases')
->join('sheets', 'sheets.wsid = purchases.wsid')
->where('uid ='.$uid)
->get();
// nb. didn't check your join() syntax, either :)
}
Probably off-topic: CodeIgniter's Active Record is more of a query builder than an implementation of Active Record. In case you're wondering. When viewed as a query builder, it makes a bit more sense ;) FWIW, I do love CodeIgniter. I jest from affection.
Query binding is the easiest to implement and does the same thing query building does - and doesn't limit you as much as you will find building does.
$query = $this->db->query('SELECT something FROM table WHERE name1=? AND name2=?', array($name1, $name2);
$result = $query->result();
http://codeigniter.com/user_guide/database/queries.html