DbiX::Class / Creating tree of test data without persisting - perl

I'm using DBIx::Class as or mapper for my Perl project. When it comes to generating test data I'm using DBIx::Class::ResultSet::new to create new entities in memory. In order to link entities with relationships I use set_from_related. This works absolutely flawless until I try to set the value(s) for a has_many relationship. Pseudo example:
# Table 'AUTHOR' has
# one-to-one (belongs_to) relationship named 'country' to table 'COUNTRY'
# one-to-many (has_many) relationship named 'books' to table 'BOOK'
my $s = Schema::getSchema();
my $author = $s->resultset('Author')->new({ name => 'Jon Doe', year_of_birth => 1982 });
my $country = $s->resultset('Country')->new({ name => 'Germany', iso_3166_code => 'DE' });
my $book = $s->resultset('Book')->new({ title => 'A star far away', publishing_year => 2002 });
# Now let's make 'em known to each other
$author->set_from_related('country', $country);
$author->set_from_related('books', $book);
# At this point
# $author->country is defined
# $author->books->first is undef <<<---- Problem
I cannot find a suitable method in the DBIx::Class::Relationship::Base documentation. The one closest to what I need is add_to_$rel but this method creates (persists) the entities. This is not an option for me as some of the entites used in my project don't belong to me (no write permission).
Does anyone have an idea how to add entities in memory for a has_many relationship ?

That's not possible as newly created result objects don't have their primary key column(s) populated until they are persisted to the database.
What you possibly want is to use multi-create and run that inside a transaction.

Related

how to get foreign key set in relationship of Laravel 9

I'm using Larave 9 to build a complicated web application.
I'm trying to get foreign key already set in relationship of a model to make another dynamic join query for some other data.
This is an example to make a dynamic query after getting foreign keys from many models:
foreach ($joinFunc as $table => $jQ){
$relTbl = $jQ['related'];
$k = $jQ['oKey'];
$fk = $jQ['fKey'];
$q->leftJoin($table, $table. '.' . $k, $relTbl. '.' .$fk);
}
My relationship in model is:
public function autopays()
{
return $this->hasMany(ClientAutoPay::class, 'client_id', 'id');
}
I want to get client_id set in the relationship
$q = Client::query()->with('autopays');
dump(
$q->first()->autopays()->getRelated()->getKeyName(),
$q->first()->autopays()->getRelated()->getForeignKey(),
);
but the result id:
"id"
"client_auto_pay_id"
what I need is to get client_id instead of client_auto_pay_id. Any help :(
I found a solution to check if the relationship has an instance ($q->whereHas()) which gave me the table name and if no instance found then I didn't pass it to dynamic joining query loop.
One thing I had to pass manually from the controller to my function and that is array of each table joining ownKeys and foreignKeys.
Now my dynamic dataTable by Livewire component is working awesome 👍

Laravel Backpack: Show a Many to Many relationship in Show Operation

Couldn't find this in the docs.
Is there any standard way, without creating a custom widget, or overriding the view template, to show a Many to Many relationships in a CRUD's showOperation in Backpack for Laravel? If the answer is NO, what would be your approach to implement it?
Let's say I have a Course Model, and a User model, and there is a Many to Many between both
class Course extends Model
{
public function students()
{
return $this->belongsToMany(User::class, 'course_students');
}
}
class User extends Model
{
public function courses()
{
return $this->belongsToMany(Course::class, 'course_students');
}
}
In the Show Operation for the Course. How do I show a Table with all students?
Indeed, you can use the relationship column for this
Excerpt:
Output the related entries, no matter the relationship:
1-n relationships - outputs the name of its one connected entity;
n-n relationships - enumerates the names of all its connected entities;
Its name and definition is the same as for the relationship field
type:
[
// any type of relationship
'name' => 'tags', // name of relationship method in the model
'type' => 'relationship',
'label' => 'Tags', // Table column heading
// OPTIONAL
// 'entity' => 'tags', // the method that defines the relationship in your Model
// 'attribute' => 'name', // foreign key attribute that is shown to user
// 'model' => App\Models\Category::class, // foreign key model
],
Backpack tries to guess which attribute to show for the related item.
Something that the end-user will recognize as unique. If it's
something common like "name" or "title" it will guess it. If not, you
can manually specify the attribute inside the column definition, or
you can add public $identifiableAttribute = 'column_name'; to your
model, and Backpack will use that column as the one the user finds
identifiable. It will use it here, and it will use it everywhere you
haven't explicitly asked for a different attribute.

Compound fields in search

I'm trying to get first name and last name from a related table
I've tried using an accessor on the related table which works fine when loading the page and for adding/editing but when searching it shows an error
Column not found: 1054 Unknown column 'FullName' in 'where clause'
This column obviously does not exist.
So I have this in my related model
{
return $this->attributes['first_name'] . ' ' . $this->attributes['last_name'];
}
And under the Backpack crud Controller I have
$this->crud->addColumn([ // n-n relationship (with pivot table)
'label' => 'Account', // Table column heading
'type' => 'select',
'name' => 'user_id', // the method that defines the relationship in your Model
'entity' => 'user', // the method that defines the relationship in your Model
'attribute' => 'FullName', // foreign key attribute that is shown to user
'model' => "App\Models\BackpackUser", // foreign key model
]);
Where am I making a mistake, input would be very much appreciated.
That needs a real column.
You can use a view (like you already do) or a model_function column to format the table cell value in your model class - https://backpackforlaravel.com/docs/3.5/crud-columns#model_function
Just see what's simpler for you.
Note: if ordering or searching doesn't work as expected see https://backpackforlaravel.com/docs/3.5/crud-columns#custom-search-logic-for-columns and https://backpackforlaravel.com/docs/3.5/crud-columns#custom-order-logic-for-columns

Entity Framework - Eager load two many-to-many relationships

Sorry for this being so long, but at least I think I got all info to be able to understand and maybe help?
I would like to load data from my database using eager loading.
The data is set up in five tables, setting up two Levels of m:n relations. So there are three tables containing data (ordered in a way of hierarchy top to bottom):
CREATE TABLE [dbo].[relations](
[relation_id] [bigint] NOT NULL
)
CREATE TABLE [dbo].[ways](
[way_id] [bigint] NOT NULL
)
CREATE TABLE [dbo].[nodes](
[node_id] [bigint] NOT NULL,
[latitude] [int] NOT NULL,
[longitude] [int] NOT NULL
)
The first two really only consist of their own ID (to hook other data not relevant here into).
In between these three data tables are two m:n tables, with a sorting hint:
CREATE TABLE [dbo].[relations_ways](
[relation_id] [bigint] NOT NULL,
[way_id] [bigint] NOT NULL,
[sequence_id] [smallint] NOT NULL
)
CREATE TABLE [dbo].[ways_nodes](
[way_id] [bigint] NOT NULL,
[node_id] [bigint] NOT NULL,
[sequence_id] [smallint] NOT NULL
)
This is, essentially, a part of the OpenStreetMap data structure. I let Entity Framework build it's objects from this database and it set up the classes exactly as the tables are.
The m:n tables do really exist as class. (I understand in EF you can build your objects m:n relation without having the explicit in-between class - should I try to change the object model in this way?)
What I want to do: My entry point is exactly one item of relation.
I think it would be best to first eager load the middle m:n relation, and then in a loop iterate over that and eager load the lowest one. I try to do that in the following way
IQueryable<relation> query = context.relations;
query = query.Where( ... ); // filters down to exactly one
query = query.Include(r => r.relation_members);
relation rel = query.SingleOrDefault();
That loads the relation and all it's 1:n info in just one trip to the database - ok, good. But I noticed it only loads the 1:n table, not the middle data table "ways".
This does NOT change if I modify the line like so:
query = query.Include(r => r.relation_members.Select(rm => rm.way));
So I cannot get the middle level loaded here, it seems?
What I cannot get working at all is load the node level of data eagerly. I tried the following:
foreach (relation_member rm in rel.relation_members) {
IQueryable<way_node> query = rm.way.way_nodes.AsQueryable();
query = query.Include(wn => wn.node);
query.Load();
}
This does work and eagerly loads the middle level way and all 1:n info of way_node in one statement for each iteration, but not the Information from node (latitude/longitude). If I access one of these values I trigger another trip to the database to load one single node object.
This last trip is deadly, since I want to load 1 relation -> 300 ways which each way -> 2000 nodes. So in the end I am hitting the server 1 + 300 + 300*2000... room for improvment, I think.
But how? I cannot get this last statement written in valid syntax AND eager loading.
Out of interest; is there a way to load the whole object graph in one trip, starting with one relation?
Loading the whole graph in one roundtrip would be:
IQueryable<relation> query = context.relations;
query = query.Where( ... ); // filters down to exactly one
query = query.Include(r => r.relation_members
.Select(rm => rm.way.way_nodes
.Select(wn => wn.node)));
relation rel = query.SingleOrDefault();
However, since you say that the Include up to ...Select(rm => rm.way) didn't work it is unlikely that this will work. (And if it would work the performance possibly isn't funny due to the complexity of the generated SQL and the amount of data and entities this query will return.)
The first thing you should investigate further is why .Include(r => r.relation_members.Select(rm => rm.way)) doesn't work because it seems correct. Is your model and mapping to the database correct?
The loop to get the nodes via explicit loading should look like this:
foreach (relation_member rm in rel.relation_members) {
context.Entry(rm).Reference(r => r.way).Query()
.Include(w => w.way_nodes.Select(wn => wn.node))
.Load();
}
Include() for some reason sometimes gets ignored when there is sorting/grouping/joining involved.
In most cases you can rewrite an Include() as a Select() into an anonymous intermediary object:
Before:
context.Invoices
.Include(invoice => invoice .Positions)
.ToList();
After:
context.Invoices
.Select(invoice => new {invoice, invoice.Positions})
.AsEnumerable()
.Select(x => x.invoice)
.ToList();
This way the query never should loose Include() information.
//get an associate book to an author
var datatable = _dataContext.Authors
.Where(x => authorids.Contains(x.AuthorId))
.SelectMany(x => x.Books)
.Distinct();

Typo3 foreign_table & foreign_table_where in TCA

I am struggling with the following problem.
I have two database tables, "Books" and "Category". I am getting all the data from "books"-table via Sysfolder in Backends List-view for editing, sorting and controlling them.
What I would like to get, is that there would be in that list view also the name of the category where the book belongs.
In "Books"-table there is a field foreign-key "category_id" which defines that for which category the Book belongs. I have tried via this "category_id" to get the name of the Category in List-view of the Books.
When I define in TCA['books'] that category_id like:
'category_id' => array (
'exclude' => 0,
'label' => 'Cat name',
'config' => array (
'type' => 'select',
'foreign_table' => 'category',
'foreign_table_where' => 'AND category.id=###REC_FIELD_category_id###',
'eval' => 'int',
'checkbox' => '0',
'default' => 0
)
),
it connects the Books and Categories using category_id (in Books-table) and uid (in Category-table).
Not like I would like, that it would connect them using category_id(in Books-table) and id(in Category-table). This id is a id of the category and can be different that uid.
Am I doing something wrong or does Typo3 somehow automatically makes this "connection" to foreign tables uid. ? Is there some way to get it like I would like?
I'm afraid it's not possible to specify different foreign key. So unless somebody proves me wrong, here is a workaround that I would use.
itemsProcFunc of the select type allows you to completely override the items in the menu and thus create a different relation.
Create an extra class that will be loaded in the backend only and that will have the method that will be called in the itemsProcFunc:
yourMethod($PA, $fobj)
Make the method to load all the categories you want to have in the SELECT box and set them in the $PA['items'] by completely overriding it so that it is an array of arrays where the 0 key is element title and 1 key is the category ID that you want. See items in select.
$PA['items'] = array(array($title, $id, ''));
Include the class in the ext_tables.php:
if(TYPO3_MODE == 'BE') require_once(t3lib_extMgm::extPath($_EXTKEY).'class.YOUR_CLASS.php');
Set the config for the category field in the books table:
'itemsProcFunc' => 'tx_YOUR_CLASS->yourMethod',
In addition to the great answer of cascaval:
#cascaval: Do you mind pointing to the select.items in the Typo3TCA in the select links? The current links aren't straight forward.
http://docs.typo3.org/typo3cms/TCAReference/singlehtml/#columns-select-properties-items
(no permission to comment to your answer, so had to answer myself just for this link ... weird)