0

N:M relation in a view

As a publisher, my catalogue has numerous Medias. Each media contains 1 or several Works (Oeuvres). This implies a N:M relationship and a pivot table (Occurences). Finally, each work is liked to his composer (Compositeur).
In each Composer's record, I need a view listing every Media with at least 1 of his Work (Oeuvre).
So, I wrote the following code in the view :

let zCompName := Name;
select 'Médias' where Occurrences.Oeuvres.Compositeur.Name = zCompName

This selects all Medias containing *only* a work of the composer. But the medias containing also works from other composers are not selected.
How to get the full list of the Medias relative to the Composer?

19 replies

null
    • Alain_Fontaine
    • 3 yrs ago
    • Reported - view

    The reason why you don't get the expected result lies in the formula "Occurrences.Oeuvres.Compositeur.Name", wich does not do what you seem to expect. When a record in the "Média" table is pointed at by a single record of the "Occurences" table, the formula returns the content of the "Name" field of the corresponding record in the "Compositeur" table. But when there are several records in the "Occurences" table pointing at a given record in the "Média" table, the formula returns an array containing the contents of all the "Name" fields of the corresponding records in the "Compositeur" table. So, in this case, your simple test for equality fails.

    What one needs to do is to test if one of the elements of the array contains the value "zCompName". Unfortunately, it seems that Ninox does not include a built-in function to perform such a test. Is there is a stealthy one, I would be glad to hear about it. If there is really no such function, my NSH opinion is that it is sorely missing.

    So, what can one do?

    1- program a loop to check all the elements of the array. Exercise left for the most intrepid readers.

    2- use this formula: contains(concat(Occurences.Oeuvres.Compositeur.Name), zCompName). This works, except that you can get spurious positives if the name of some composer is a substring of another one. For example, if you have a composer named "Carl" and another one named "Carlos", looking for the first one will also find the occurences of the second one.

    3- use the more convoluted:

    let zCompName := " | " + Name + " | ";
    (select 'Médias')[contains(" | " + join(Occurences.Oeuvres.Compositeur.Name, " | ") + " | ", zCompName)]

    where " | " may be replaced by any string not appearing in any composer name. You could still get unexpected results in the - admittedly rather pathological - case of several composers having the same name. If necessary, this can be avoided by working with unique identifiers rather than with the names.

    • Alain_Fontaine
    • 3 yrs ago
    • Reported - view

    Following on my own post, sorry.

    I mentioned the possibility to work with unique identifiers, without giving a possible formula. It goes like this:

    let zCompId := ", " + text(Id) + ", ";
    (select 'Médias')[contains(", " + concat(Occurences.Oeuvres.Compositeur.Id) + ", ", zCompId)]

    It should probably be the preferred solution.

    • Alain.2
    • 3 yrs ago
    • Reported - view

    Your solution is not only perfect but also educational. You have just removed a very annoying little pebble from my shoe!
    The last solution works as a dream. (I hope that Ninox will soon offer an easier way to manage N:M links).
    Thank you so much for your time and expertise.

    • Alain.2
    • 3 yrs ago
    • Reported - view

    Hello, I am back with an update to this old thread. So, with the following formula, I get the Médias relative to a Composer :

    let zCompName := “ | ” + Name + “ | ”;
    (select “Médias”)[contains(“ | ” + join(Occurences.Oeuvres.Compositeur.Name, “ | ”) + “ | ”, zCompName)]

    Now, I need to have the sales relative to all these medias for a specific year. The following code is buggy but I do not see why :

    let zCompName := “ | ” + Name + “ | ”;

    select Sales where (select “Médias”)[contains(“ | ” + join(Occurrences.Oeuvres.Compositeur.Nom, “ | ”) + “ | ”, zCompName)] and year(Invoice.Date_invoice) = zYear

    Any help highly appreciated!

    • Alain_Fontaine
    • 3 yrs ago
    • Reported - view

    Cascading select commands on a single line should better be avoided. Reasoning on the context of each evaluation tends to become intractable.

    What are the relations between the tables “Invoice”, “Sales” and the other tables?

    • Alain.2
    • 3 yrs ago
    • Reported - view

    Thank you for your reaction. (I worked previously with 4D where it was possible to store intermediate queries, which makes complex queries easier to build.)

    [Sales] is a children of [Invoice] and is linked to [Médias]. The Date is a field of [Invoice].

    • Alain_Fontaine
    • 3 yrs ago
    • Reported - view

    When looking again at an old question, one often sees new options. So please let me first look back at the last july problem. It will help for solving the new one. In the Ninox realm, the “select” command allows one to grab records from anywhere in the database, at the expense of some complexity. But when “hard” relations have been defined by inserting reference fields, it is often easier to use those relations to access... related data.
    So, in the case of the view listing the “Médias” items containing at least one “Œuvre“ from the current “Compositeur”, one can simply follow the references materializing the relations:
    Oeuvres.Occurences.“Médias”
    This expression “follows all the references” from the current “Compositeur”, and returns an array containing handles to all the records in the “Médias” table that sit at the end of an instance of that chain of references. Now, since a given “Média” may contain several “Œuvres“ from the same composer, following all the references may produce several instances of the same “Média”. Fortunately, Ninox has a function to solve that issue. And so, in the definition of the view, one can simply use the expression:
    unique(Oeuvres.Occurences.“Médias”)
    which is much simpler than the script using the “select” command.
    Now, what about the view listing the “Sales” of “Médias“ containing at least one “Œuvre“ from the current ”Compositeur“? On can use the same logic, following a chain of references, but now the chain is longer, and a condition must be added:
    Oeuvres.Occurences.”Médias“.Sales[year(Invoice.Date_invoice) = zYear]
    In this expression, when the condition is evaluated, we are in the context of a canditate “Sale“, and so ”Invoice.Date_invoice“ follows the reference to the ”Date_invoice“ field of the “Invoice” which is the parent of that candidate “Sale”. Phew...
    Once again, the same “Sale” can appear mutiple times, and once again the “unique” function saves the day. The final expression to populate the view is thus:
    unique((Oeuvres.Occurences.“Médias”.Sales[year(Invoice.Date_invoice) = zYear])
    BTW: it is possible in Ninox to save the result of a “select” query:
    let zResult := select .....
    “zResult” now contains an array of handles to the records grabbed by the “select” command.
    CAUTION: the nice forum software has probably changed the single quotes around “Médias” into double quotes....

    • Alain.2
    • 3 yrs ago
    • Reported - view

    Thank you very much for this important development. Putting the calculation back into the composer“s table and using the ”single“ command offers a lighter perspective.

    However, I still have two problems:

    1. On typing:

    unique((Oeuvres.Occurences.“Médias”.Sales[year(Invoice.Date_invoice) = zYear])

    Ninox asks to place a comma before the last parenthesis of the line, which I don”t understand. (I have indeed replaced double quotation marks with singles).

    2. Once the view has been created, would it be possible to export it in order to have a table of annual royalties for each composer: [composer; year; amount of royalties]?

    I am really measuring the value of the help you are giving me.

    • Alain_Fontaine
    • 3 yrs ago
    • Reported - view

    Strange… I have made a local mockup of your database to test the function, and it works. But it may be that I did not correctly understand the structure of your database. Since uploading images in this forum is broken, I have put an image of my vision of the database here: https://app.box.com/s/3kmvm5sy9rj2gc0fyxuqp4hlknqypay3. Could you verify that it corresponds to your database? You may have to copy the link and paste it in your browser, since clicking on links in the forum also seems to be broken.

    • Alain_Fontaine
    • 3 yrs ago
    • Reported - view

    There should be only one opening parenthesis between “unique” and “Œuvres”.

    • Alain.2
    • 3 yrs ago
    • Reported - view

    Dear Alain, your diagram is perfect and, indeed, the problem was simply that of the parenthesis. I must admit that my base is far from being finished, at least with me as a developer.
    I was wondering if you were a professional developer. If so, I would be happy to consider a business relationship with you to solve the remaining problems: establishing an annual inventory statement and finalizing the copyright calculation.
    You can contact me by email: infoATnewconsonantmusic.com.
    Thank you in any case for your past and present help.

    • Alain_Fontaine
    • 3 yrs ago
    • Reported - view

    I am not a professional developer, but a just retired ICT guy having fun with a new toy, and trying to give some help by sharing a few tricks... So, with this new year, I hope that I will be able to complement past and present help with some amount of future help.

    • Frank_Bohmer
    • 3 yrs ago
    • Reported - view

    alert(“Hello”)

    • Alain.2
    • 3 yrs ago
    • Reported - view

    I finally managed to fix the issue thanks to Alain (and the end of the year days-off). Here is my button script for anyboby needing some inspiration :

    let xCurrRec := ID;
    let xYear := Year;
    "// Select Medias ready for inventory //";
    for i in select Medias where text(Step) = "Inventaire" and 'Année_sortie' <= xYear do
    "// Create a subrecord for each Media //";
    let x := (create InventaireLigne);
    x.(Inventaire := xCurrRec);
    x.(Year := xYear);
    x.(Media := i);
    "// *** variations annuelles *** //";
    let xVentes := sum((select Ventes where Media = i and year(Factures.Date) = xYear).Nb);
    x.(Nb_VentesAn := xVentes);
    let yVentes := sum((select Ventes where Media = i and year(Factures.Date) = xYear).Prix_total);
    x.(VentesAn := yVentes);
    let xHommages := sum((select Hommages where Media = i and year(Date_hommage) = xYear).Nb);
    x.(Nb_HommagesAn := xHommages);
    let xTirages := sum((select Tirages where 'Médias' = i and Nb > 0 and year(Date_tirage) = xYear).Nb);
    x.(Nb_TiragesAn := xTirages);
    let xPilons := -1 * sum((select Tirages where 'Médias' = i and Nb < 0 and year(Date_tirage) = xYear).Nb);
    x.(Nb_PilonsAn := xPilons);
    let xFraisProduction := sum((select Tirages where 'Médias' = i and Nb > 0 and year(Date_tirage) = xYear).Cout);
    x.(Frais_ProdAn := xFraisProduction);
    "// *** état final du stock *** //";
    let zVentes := sum((select Ventes where Media = i).Nb);
    x.(Nb_VentesTot := zVentes);
    let zHommages := sum((select Hommages where Media = i).Nb);
    x.(Nb_HommagesTot := zHommages);
    let zTirages := sum((select Tirages where 'Médias' = i and Nb > 0).Nb);
    x.(Nb_TiragesTot := zTirages);
    let zPilons := sum((select Tirages where 'Médias' = i and Nb < 0).Nb);
    x.(Nb_PilonsTot := zPilons);
    let zFraisProduction := sum((select Tirages where 'Médias' = i and Nb > 0).Cout);
    x.(Frais_ProdTot := zFraisProduction)
    end

    (Well, I have other small problems but this will be for a new thread...)

    • Alain.2
    • 3 yrs ago
    • Reported - view

    A last (?) problem connected with this case...

    I have to draw up a sales statement for each composer in order to calculate his yearly royalties. In the [Redevances] table, I have placed a button in charge of creating a sub-record [RedevancesLignes] for each [Media] of the composer (this works... thank you Alain!). The script will then search for the sales made for each media and calculate the number and the amount of sales [Ventes]. However, only certain sales are taken into account. I've been looking since yesterday why many sales are ignored. I see no difference between the selected sales and the ignored ones. Do you understand the error in my script?

    [Ventes] is linked to [RedevancesLignes] which is a sub-table of [Redevances].

    let xCurrRec := ID;
    let xYear := 'Année';
    let zComposer := "-" + Composer_No + "-";
    "// Select Medias in production for inventory //";
    for i in (select Medias where text(Production_state) = "Inventaire")[contains(Compositeurs_No, zComposer)] do
    "// Create a subrecord for each Redevance //";
    let x := (create RedevancesLignes);
    x.(Redevances := xCurrRec);
    x.(Year := xYear);
    let yComposer := Compositeurs.Nom;
    x.(Composer := yComposer);
    let yTitle := i.Titre;
    x.(Title := yTitle);
    let yPrice := i.'Prix_détail';
    x.(Price := yPrice);
    let yProrata := i.Prorata;
    x.(Prorata := yProrata);
    "// *** Calculate each royalties PROBLEM HERE *** //";
    let xVentes := sum((select Ventes where Media = i and Hors_redevances = false and year(Factures.Date) = xYear).Nb);
    x.(Sales := xVentes);
    let xSalesValue := sum((select Ventes where Media = i and Hors_redevances = false and year(Factures.Date) = xYear).Prix_total);
    let xRoyalties := xSalesValue / (10 * yProrata);
    x.(Royalties := xRoyalties)
    end

    • Alain_Fontaine
    • 3 yrs ago
    • Reported - view

    It's hard to debug something without actually seeing it... I suppose that the schema has not changed, except that the "Sales" table is now called "Ventes", and the "Invoice" table is now called "Factures". What I would do to debug the issue would be to create a view and populate it with the problematic select statement. This would give a real-time view of the selected records. Then, since there are three conditions that must be met for a record to be selected, I would try to remove one of them to see what happens. Of course, too many records would then be selected, but by studying the actual selection one would be able to determine if the now removed condition is the culprit. Hope this helps.

    • Alain.2
    • 3 yrs ago
    • Reported - view

    Indeed, I removed each condition (even rearranged them) without obvious solution (to my eyes). I put a copy of the base here if you have any time to investigate :

    <https://drive.google.com/drive/folders/1q-O4BvCV-hjk1N2HiYOc7lmDNAglj1hu?usp=sharing>

    Not every composer has sales... so the best is to go to Compositeurs > Lysight > tag Redevances > 2020 > button [Créer redevances annuelles du compositeur] (2 minutes needed)

    For example, the sales for "35 Rhythmic Studies" do not appear !

    Still investigating...

    Thank you for your time and concern!

    • Alain_Fontaine
    • 3 yrs ago
    • Reported - view

    The problem seems to come from the "Hors_redevances" field. This field is empty in some records of the "Ventes" table. You can correct the isue by updating those records with the value "no", and avoid future trouble by defining a default value - I suppose "no" - in the field definition.

    • Alain.2
    • 3 yrs ago
    • Reported - view

    Indeed, there was the problem! The field looks the same with a negative value and no value at all. It's fantastic to have found this. Thank you very much!
    Send me your postal address to info@newconsonantmusic.com. I'd be happy to send you some records... so that you see what you contributed to.
    Thank you very much again.

Content aside

  • 3 yrs agoLast active
  • 19Replies
  • 1906Views