I am building an app, where an account can have many services, all the information is related to a service. In example:
Account A has 3 services and each service has pages.
In order to avoid someone modifying the service_id when saving a page, at the moment I do:
if(Yii::$app->request->isPost) {
$post = Yii::$app->request->post();
$model->load($post);
$model->service_id = $this->service->id;
}
Where $model->service_id = $this->service->id helps me assign the selected service_id after loading table to model and avoid someone sending service_id from the form.
But in case someone in the future, develops "documents" I would like to avoid the developer handling this service_id on all the queries.
So First it I thought I could try:
public function beforeFind($queryData) {
parent::beforeFind();
$queryData['conditions'] = array('service_id' => 2);
return $queryData;
}
But still needs the developer to implement it. So maybe is there a way to create a "BaseService" model where all other service related models should extend from but not sure how to:
Add the condition from the parent model?
How to pass the id to this model so it keeps it during all queries?
Maybe there is a simple solution, and I am overcomplicating myself due long hours working, not sure.
That is a default condition to apply for all queries. In case your application is built on top of ActiveRecord classes (not performing direct SQL queries or on the fly QueryBuilder) then you can simply override the find() method inside your Model class:
public static function find()
{
/* you can add more dynamic logic here */
return parent::find()->where(['service_id' => 2]);
}
By default, all controllers in Yii2 are using Model::find() to retrieve data from database, adding such condition should be enough to not retrieve anything with a different service_id than 2. Direct http GET requests by ID should then output 404 if that condition isn't satisfied and retrieving them as relational data within a different model class should return a filtered array.
IMPORTANT: To not break that implementation you need to:
Always use
ActiveRecord. Otherwise you'll need to manually add the condition to your queries.
(This is not correct) Be carful on when to use asArray() as it omits ActiveRecord features (See note and explanation
here). Otherwise you need to manually re-declare the condition like: Account::find()->where(['service_id' => 2])->asArray()->all();
Always use andWhere() to merge conditions because where() will override/ignore the default one. Example: Account::find()->andWhere('age>30')->all();
to reuse such filters you can put them into a custom ActiveQuery.
in your ActiveRecord:
public static function find() {
return (new ActiveQuery(get_called_class()));
}
ActiveQuery:
public function service($service = 2) {
return $this->andWhere(['service_id' => $service]);
}
your model based on your ActiveRecord:
public static function find() {
return (new ActiveQuery(get_called_class()))->service(2);
}
alternatively
$model->find()->service(1);
also, this might be of interest (setting Default values per scenario)
Related
My models have both id and counter attributes. The id is a UUID, and the counter is an integer which is auto-incremented by the database.
Both are unique however I rely on id as the primary key. The counter is just a human-friendly name that I sometimes display to the user.
Immediately before an object is created a listener gives it a UUID. This works fine.
When the record is saved, MySQL increments the counter field. This works fine except that the copy of the object which I have in memory does not have the counter value. I can reload the object to find out what its counter is, but that would require another database query.
Is there a way to find the value of the counter without a specific database query? For example, is it returned as part of the response from the database when a record is created?
Few things:
Use create(array $attributes) and you'll get exactly what you want. For this having right, you have to ensure that $fillable array consists all attributes' names passed to create method.
You should use Observer on model instead of listener (most likely creating method).
Personal preference using Eloquent is that you should use id for id (increment field) and forget custom settings between models because by default it is what relations expect and so on
public function secondModels()
{
return $this->hasMany(SecondModel::class);
}
is pretty much no brainer. But for having this working best way would be (also following recommendations of this guy) FirstModel::id, SecondModel::id, SecondModel::first_model_id; first_models, second_models as table names. Avoiding and/or skipping this kind of unification is lot of custom job afterward. I don't say it can't be done but it is lot of non-first-time-successful work done.
Also, if you want visitor to get something other than id field name, you can make computed field with accessor:
/**
* Get the user's counter.
*
* #return string
*/
public function getCounterAttribute(): string
{
return (string)$this->id;
}
Which you call then with $user->counter.
Also personal preference of mine is to have most possible descriptive variable names so uuid field of mine would be something like
$table->uuid('uuid4');
This is some good and easy to make practice of Eloquent use.
Saying all this let me just to say that create() and save() will return created object from database while insert() shall not do it.
How to select a single row on october cms?
How can a simple thing be so complicated here?
I thought it would be something to help us and not to disturb something that is as simple as
SELECT * FROM `engegraph_forms_membros`
Here it's like fighting against demons without a bible, oh god why?
Why make the query difficult for newbie?
I understand you don't speak English natively but you should watch every single one of these videos.
Does the record belong to a model in a plugin? Here are the docs on how to work with models.
You make a plugin, set the database which creates models, and then make components to be ran in your CMS Pages.
In a component.php file you can have something like this: Here I am calling the model class Agreements with use Author\Plugin\Models\Agreements;. This allows me to run a function/method to retrieve all agreements or one agreements using laravel's eloquent collection services.
Lets say we have the ID of a record. Well we can either call on the Agreements model with ::find or with ::where. You will noticed I have two functions that essentially do the same thing. ::find uses the primary key of the models (in my case the id) and will return a singular record. *Note that find can take an array and return a collection of records; like ::where. Using ::where we are going to look for the ID. *Note ::where always returns a collection which is why I have included ->first().
<?php namespace Author\Plugin\Components;
use Session;
use Input;
use Crypt;
use Db;
use Redirect;
use Illuminate\Contracts\Encryption\DecryptException;
use October\Rain\Support\Collection;
use Author\Plugin\Models\Agreements;
class GetAgreement extends \Cms\Classes\ComponentBase
{
public function componentDetails()
{
return [
'name' => 'Get one agreement',
'description' => 'Get an agreement to change or delete it'
];
}
public function onRun() {
$this->page['agreement'] = $this->getWithFindAgreement;
}
public function getWithFindAgreement() {
$id = 1;
$agreement = Agreements::find($id);
return $agreement;
}
public function getWithWhereAgreement() {
$id = 1;
$agreement = Agreements::where($id)->first();
return $agreement;
}
}
If for some reason you aren't working with models, here are the docs to work with Databases. You will have to register the use Db; facade.
Here call the table you want and use ::where to query it. *Note the use of ->first() again.
$users = Db::table('users')->get();
$user = $users->where('id', 1)->first();
There are two simple ways to select a single row:
This will give you the'first' record in the selected recordset.
SELECT top 1 * FROM `engegraph_forms_membros`
This will select all the records that meet the predicate requirement that the value of <columnname> is equal to <value>
SELECT * FROM `engegraph_forms_membros` where <columnname>=<value>
If you select a record where multiple values meet that requirement, then you can (randomly) pick one by combining the solutions...
SELECT top 1 * FROM `engegraph_forms_membros` where <columnname>=<value>
But be aware that without an ORDER BY clause, the underlying data is unordered and prone to change uncontrollably, which is why most people (including your boss) will find the use of 'Top' to be improper for real use.
I have lightswitch entities created in the HTML5 client. There is a field that the user should not be setting that needs to be set based on their login. In this case, the client the user is associated with.
The standard examples (in Michael Washington's book Creating HTML 5 Websites ... Using Lightswitch and all over the web) involve assigning the user name as the relevant field, setting up a little handler on the server to return the relevant field using an AJAX call.
This is was all well and good while prototyping, but now that we are doing this for real, there is a relationship to another entity involved, the Client Entity, so you can't just assign the Client Id. So instead of simply assigning a Client ID, now we have associate a whole Client Object to the entity.
Here the suggestion is to do what ends up looking like this:
myapp.activeDataWorkspace.ApplicationData.Clients_SingleOrDefault(1).execute().then(function (ClientQuery) {
entity.setClient(ClientQuery.results[0]);
});
My problem is with this part:
Clients_SingleOrDefault(1)
I need to get that number dynamically, not just a hard coded 1, as that OP suggested. So I can do it in two server calls, one to get the ID, and then the next one to substitute that result into the next call, but that seems ... inefficient.
msls.promiseOperation(CallGetClientId).then(function PromiseSuccess(PromiseResult) {
myapp.activeDataWorkspace.ApplicationData.Clients_SingleOrDefault(Number(PromiseResult)).execute().then(function (ClientQuery) {
entity.setClient(ClientQuery.results[0]);
});
});
function CallGetClientId(operation) {
$.ajax({
type: 'post',
data: {},
url: '../UserCode/GetClientId.ashx',
success: operation.code(function AjaxSuccess(AjaxResult) {
operation.complete(AjaxResult);
})
});
}
It would seem that there should be a better way to do it. Is there?
The simplest method of achieving this with one server call is to implement a scalar query.
For example, if you create a scalar query called ClientForCurrentUser against your Clients table you'd be able to make a single client side call as follows:
myapp.activeDataWorkspace.ApplicationData.ClientForCurrentUser().execute().then(function (query) {
if (query && query.results && query.results.length !== 0) {
entity.setClient(query.results[0]);
}
});
As regards configuring the scalar query, the following post covers an example (note the setting of the 'Number of Results Returned' property):
Applying where clause to child collection in LightSwitch query
Then, assuming you have a field against your Client record that relates it to the current username (e.g. Client.RelatedUser) you'd implement the following type of PreprocessQuery method against the scalar query:
partial void ClientForCurrentUserPersonForCurrentUser_PreprocessQuery(ref IQueryable<Client> query)
{
var username = this.Application.User.Name;
query = query.Where(c => c.RelatedUser.Equals(username, StringComparison.OrdinalIgnoreCase)).Take(1);
}
Circumstances
I have three models/db-tables related with 1:n each: An order has multiple commissions and a commission has multiple commission_positions. Therefore the commission_position has an FK-field containing a commission id. The commission itself has an FK-field containing the id of an order.
Order > Commission > CommissionPositions
Problem
What I need to do now is select all the CommissionPositions having a certain value in the related Order-Model. Obvious solution is to use the Query-Object of CommissionPosition which I extended with a named scope. The named scope looks like this:
class CommissionPositionQuery extends ActiveQuery
{
/**
* Named scope to filter positions of a certain alpha order id
* #param integer $id the alpha order id
* #return \common\models\query\CommissionPositionQuery
*/
public function alphaOrderId($id)
{
//TODO: with not working
$this->with(['commission.order']);
$this->andWhere(['alpha_order_id'=>$id]);
return $this;
}
}
The relation commission is defined on the Commission-Model and working. The second relation order is defined on the commission-model and working as well. The filtered field alpha_order_id is in the Order-Table. Now I execute the query like this:
$filteredPositions = CommissionPosition::find()->alphaOrderId(17)->all();
The scope is called successfully and the where-part is used, but when I check the generated SQL I see no join-statements even though I use the with-method to tell yii to fetch the relation together. The response is 'unknown column alpha_order_id' which makes sense as there is no join to the related tables. This is the generated SQL:
SELECT * FROM `commission_position` WHERE (`alpha_order_id`=17)
What am I missing? Is this a bug of Yii2?
Found the soution myself. The naming changes between Yii1 and Yii2 lead to a little confusion. To prevent others from wasting time here the details:
Yii1
In yii 1 you would join in a relation (exemplary: commission) directly like this:
$query->with = 'commission'
$query->together = true;
Yii2 / difference
When calling the with-method like showed in the question the relation was successfully added to the with-array of the ActiveQuery. However, when executing the query, the join part was missing.
Solution
Seems like the with-method is NOT the way to go. Instead I used the method called joinWith with the following signature:
public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN')
Now as described in the answer I defined the relation as the first argument ('commission.order') and left the rest as is, because the default values are perfectly fine. Pay attention to the default value of the second parameter. this makes sure the relations are joined in directly!
VoilĂ ...the resulting sql contains the needed joins. One pitfall is to be considered though: Ambigious column namings is of course to be handled by ourselves! Link to the documentation of the method:
http://www.yiiframework.com/doc-2.0/yii-db-activequery.html#joinWith()-detail
If you want a JOIN use:
$this->joinWith(['commission.order']);
I am working on a Dynamic data.
after creating a dynamic model and registering in global.asax, like
DefaultModel.RegisterContext(typeof(masterEntities1),new ContextConfiguration() { ScaffoldAllTables = true });
when i run an application, it shows a list of tables but when i click any of the table it throws an exception:
The method 'Skip' is only supported for sorted input in LINQ to Entities. The method 'OrderBy' must be called before the method 'Skip'.
but i haven't declare any query into my application.
You must call .OrderBy' on your query if you use the .Skip method. For example if you were using something similar to the following:
results = results.Skip(pageNumber * size).Take(size);
In the case above you would have previously had to use the .OrderBy to order the query if you are planning on using paging methods or something of the like. If you have an Id field, adding this onto your original query expression should eliminate the error:
.OrderBy(x => x.Id);