[{"data":1,"prerenderedAt":1454},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fdefine-layer-relations-pyqgis":3},{"id":4,"title":5,"body":6,"description":1443,"extension":1444,"meta":1445,"navigation":253,"path":1450,"seo":1451,"stem":1452,"__hash__":1453},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fdefine-layer-relations-pyqgis\u002Findex.md","Define Layer Relations in PyQGIS",{"type":7,"value":8,"toc":1429},"minimark",[9,13,22,35,195,200,217,221,423,457,460,493,498,502,527,544,547,551,671,807,830,834,837,1013,1018,1021,1025,1028,1105,1122,1126,1129,1256,1275,1283,1287,1300,1304,1352,1356,1359,1363,1369,1375,1381,1391,1395,1425],[10,11,5],"h1",{"id":12},"define-layer-relations-in-pyqgis",[14,15,16,17,21],"p",{},"A relation tells QGIS that rows in one layer belong to rows in another — inspections to a parcel, readings to a gauge, defects to a pipe. Once one exists, the parent's attribute form grows a table of its children, ",[18,19,20],"code",{},"relation_aggregate"," starts working, and cascading delete becomes possible. None of that happens automatically from a foreign key in the data; you have to declare it.",[14,23,24,25,30,31,34],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002F","Working with QGIS Projects",". It covers building a ",[18,32,33],{},"QgsRelation",", the direction of the field mapping, relation strength, registering it in the project, and reading related features from Python.",[14,36,37],{},[38,39,44,48,52,59,76,85,95,101,108,114,120,124,127,131,136,141,145,149,153,155,158,160,163,166,172,176,185,190],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 326","img","A one-to-many relation showing the referenced field on the parent layer and the referencing field on the child layer, with the direction of the reference","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"Referencing points at referenced",[49,50,51],"desc",{},"The parent layer holds the referenced field, usually its primary key. The child layer holds the referencing field, the foreign key. The relation records that the child's field points at the parent's field, and QGIS uses that to show each parent's children in its form and to resolve relation aggregates.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","326","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"relArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The child does the referencing",[53,86],{"x":87,"y":88,"width":89,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"30","50","290","176","10","#eef7f4","#0f766e","stroke-width:2.5",[77,96,100],{"x":97,"y":98,"style":99,"fill":93,"textAnchor":83},"175","76","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","parcels — the parent",[53,102],{"x":103,"y":104,"width":105,"height":80,"fill":106,"stroke":93,"style":107},"52","94","246","#e8efe6","stroke-width:1.4",[77,109,113],{"x":110,"y":111,"style":112,"fill":75},"64","112","font-size:10px;font-family:monospace","parcel_id     use_class",[53,115],{"x":103,"y":116,"width":105,"height":117,"fill":118,"stroke":93,"style":119},"122","24","#fffdf7","stroke-width:1.2",[77,121,123],{"x":110,"y":122,"style":112,"fill":75},"139","P-1043        residential",[53,125],{"x":103,"y":126,"width":105,"height":117,"fill":118,"stroke":93,"style":119},"148",[77,128,130],{"x":110,"y":129,"style":112,"fill":75},"165","P-1044        retail",[77,132,135],{"x":97,"y":133,"style":134,"fill":93,"textAnchor":83},"200","text-anchor:middle;font-size:10px;font-family:sans-serif","referencedField = \"parcel_id\"",[53,137],{"x":138,"y":88,"width":89,"height":90,"rx":91,"fill":139,"stroke":140,"style":94},"440","#eff3ff","#2563eb",[77,142,144],{"x":143,"y":98,"style":99,"fill":140,"textAnchor":83},"585","inspections — the child",[53,146],{"x":147,"y":104,"width":105,"height":80,"fill":148,"stroke":140,"style":107},"462","#dbeafe",[77,150,152],{"x":151,"y":111,"style":112,"fill":75},"474","parcel_fk     inspected_on",[53,154],{"x":147,"y":116,"width":105,"height":117,"fill":118,"stroke":140,"style":119},[77,156,157],{"x":151,"y":122,"style":112,"fill":75},"P-1043        2026-03-11",[53,159],{"x":147,"y":126,"width":105,"height":117,"fill":118,"stroke":140,"style":119},[77,161,162],{"x":151,"y":129,"style":112,"fill":75},"P-1043        2026-07-02",[77,164,165],{"x":143,"y":133,"style":134,"fill":140,"textAnchor":83},"referencingField = \"parcel_fk\"",[167,168],"line",{"x1":169,"y1":170,"x2":57,"y2":170,"stroke":75,"style":171},"436","138","stroke-width:2.4;marker-end:url(#relArrow)",[77,173,175],{"x":79,"y":174,"style":134,"fill":75,"textAnchor":83},"128","points at",[53,177],{"x":178,"y":179,"width":180,"height":181,"rx":67,"fill":182,"stroke":183,"style":184},"80","248","600","62","#fdf2e2","#b45309","stroke-width:2",[77,186,189],{"x":79,"y":187,"style":188,"fill":183,"textAnchor":83},"272","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","get these two the wrong way round and the relation silently finds nothing",[77,191,194],{"x":79,"y":192,"style":193,"fill":75,"textAnchor":83},"294","text-anchor:middle;font-size:10.5px;font-family:sans-serif","addFieldPair(referencingField, referencedField) — child first",[196,197,199],"h2",{"id":198},"prerequisites","Prerequisites",[201,202,203,211,214],"ul",{},[204,205,206,210],"li",{},[207,208,209],"strong",{},"QGIS 3.34 LTR"," or newer.",[204,212,213],{},"Two layers loaded in the project with a shared key — a parent and its children.",[204,215,216],{},"The parent's key field should be unique; QGIS does not enforce it and a duplicate produces children attached to several parents.",[196,218,220],{"id":219},"build-and-register-the-relation","Build and register the relation",[222,223,228],"pre",{"className":224,"code":225,"language":226,"meta":227,"style":227},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsRelation\n\nproject = QgsProject.instance()\nparcels = project.mapLayersByName(\"parcels\")[0]\ninspections = project.mapLayersByName(\"inspections\")[0]\n\nrelation = QgsRelation()\nrelation.setId(\"parcel_inspections\")\nrelation.setName(\"Inspections\")\nrelation.setReferencedLayer(parcels.id())\nrelation.setReferencingLayer(inspections.id())\nrelation.addFieldPair(\"parcel_fk\", \"parcel_id\")\n\nif not relation.isValid():\n    raise SystemExit(\"relation is not valid — check layer ids and field names\")\n\nproject.relationManager().addRelation(relation)\n","python","",[18,229,230,248,255,267,291,310,315,326,338,349,355,361,378,383,395,412,417],{"__ignoreMap":227},[231,232,234,238,242,245],"span",{"class":167,"line":233},1,[231,235,237],{"class":236},"snl16","from",[231,239,241],{"class":240},"s95oV"," qgis.core ",[231,243,244],{"class":236},"import",[231,246,247],{"class":240}," QgsProject, QgsRelation\n",[231,249,251],{"class":167,"line":250},2,[231,252,254],{"emptyLinePlaceholder":253},true,"\n",[231,256,258,261,264],{"class":167,"line":257},3,[231,259,260],{"class":240},"project ",[231,262,263],{"class":236},"=",[231,265,266],{"class":240}," QgsProject.instance()\n",[231,268,270,273,275,278,282,285,288],{"class":167,"line":269},4,[231,271,272],{"class":240},"parcels ",[231,274,263],{"class":236},[231,276,277],{"class":240}," project.mapLayersByName(",[231,279,281],{"class":280},"sU2Wk","\"parcels\"",[231,283,284],{"class":240},")[",[231,286,55],{"class":287},"sDLfK",[231,289,290],{"class":240},"]\n",[231,292,294,297,299,301,304,306,308],{"class":167,"line":293},5,[231,295,296],{"class":240},"inspections ",[231,298,263],{"class":236},[231,300,277],{"class":240},[231,302,303],{"class":280},"\"inspections\"",[231,305,284],{"class":240},[231,307,55],{"class":287},[231,309,290],{"class":240},[231,311,313],{"class":167,"line":312},6,[231,314,254],{"emptyLinePlaceholder":253},[231,316,318,321,323],{"class":167,"line":317},7,[231,319,320],{"class":240},"relation ",[231,322,263],{"class":236},[231,324,325],{"class":240}," QgsRelation()\n",[231,327,329,332,335],{"class":167,"line":328},8,[231,330,331],{"class":240},"relation.setId(",[231,333,334],{"class":280},"\"parcel_inspections\"",[231,336,337],{"class":240},")\n",[231,339,341,344,347],{"class":167,"line":340},9,[231,342,343],{"class":240},"relation.setName(",[231,345,346],{"class":280},"\"Inspections\"",[231,348,337],{"class":240},[231,350,352],{"class":167,"line":351},10,[231,353,354],{"class":240},"relation.setReferencedLayer(parcels.id())\n",[231,356,358],{"class":167,"line":357},11,[231,359,360],{"class":240},"relation.setReferencingLayer(inspections.id())\n",[231,362,364,367,370,373,376],{"class":167,"line":363},12,[231,365,366],{"class":240},"relation.addFieldPair(",[231,368,369],{"class":280},"\"parcel_fk\"",[231,371,372],{"class":240},", ",[231,374,375],{"class":280},"\"parcel_id\"",[231,377,337],{"class":240},[231,379,381],{"class":167,"line":380},13,[231,382,254],{"emptyLinePlaceholder":253},[231,384,386,389,392],{"class":167,"line":385},14,[231,387,388],{"class":236},"if",[231,390,391],{"class":236}," not",[231,393,394],{"class":240}," relation.isValid():\n",[231,396,398,401,404,407,410],{"class":167,"line":397},15,[231,399,400],{"class":236},"    raise",[231,402,403],{"class":287}," SystemExit",[231,405,406],{"class":240},"(",[231,408,409],{"class":280},"\"relation is not valid — check layer ids and field names\"",[231,411,337],{"class":240},[231,413,415],{"class":167,"line":414},16,[231,416,254],{"emptyLinePlaceholder":253},[231,418,420],{"class":167,"line":419},17,[231,421,422],{"class":240},"project.relationManager().addRelation(relation)\n",[14,424,425,428,429,432,433,437,438,441,442,445,446,448,449,452,453,456],{},[207,426,427],{},"Breakdown:"," ",[18,430,431],{},"addFieldPair"," takes the ",[434,435,436],"em",{},"referencing"," field first and the ",[434,439,440],{},"referenced"," field second — child, then parent — and reversing them produces a relation that validates and finds nothing, which is the single most common mistake here. The layers are identified by id rather than by object, so both must already be in the project. ",[18,443,444],{},"setId"," is what ",[18,447,20],{}," and the project file refer to, so give it something stable and readable rather than letting QGIS generate one; ",[18,450,451],{},"setName"," is the label a user sees. ",[18,454,455],{},"isValid()"," checks that both layers exist and both fields resolve, and it is worth asserting before registering.",[14,458,459],{},"A composite key is expressed as several field pairs:",[222,461,463],{"className":224,"code":462,"language":226,"meta":227,"style":227},"relation.addFieldPair(\"site_fk\", \"site_id\")\nrelation.addFieldPair(\"year_fk\", \"year\")\n",[18,464,465,479],{"__ignoreMap":227},[231,466,467,469,472,474,477],{"class":167,"line":233},[231,468,366],{"class":240},[231,470,471],{"class":280},"\"site_fk\"",[231,473,372],{"class":240},[231,475,476],{"class":280},"\"site_id\"",[231,478,337],{"class":240},[231,480,481,483,486,488,491],{"class":167,"line":250},[231,482,366],{"class":240},[231,484,485],{"class":280},"\"year_fk\"",[231,487,372],{"class":240},[231,489,490],{"class":280},"\"year\"",[231,492,337],{"class":240},[14,494,495,497],{},[207,496,427],{}," All pairs must match for a child to belong to a parent, which is an AND rather than an OR. Composite relations work everywhere single-field ones do, but the attribute form's relation editor handles them less gracefully, so a surrogate single key is worth considering if users will edit through the form.",[196,499,501],{"id":500},"relation-strength-and-deletion","Relation strength and deletion",[222,503,505],{"className":224,"code":504,"language":226,"meta":227,"style":227},"from qgis.core import Qgis\n\nrelation.setStrength(Qgis.RelationshipStrength.Composition)\n",[18,506,507,518,522],{"__ignoreMap":227},[231,508,509,511,513,515],{"class":167,"line":233},[231,510,237],{"class":236},[231,512,241],{"class":240},[231,514,244],{"class":236},[231,516,517],{"class":240}," Qgis\n",[231,519,520],{"class":167,"line":250},[231,521,254],{"emptyLinePlaceholder":253},[231,523,524],{"class":167,"line":257},[231,525,526],{"class":240},"relation.setStrength(Qgis.RelationshipStrength.Composition)\n",[14,528,529,531,532,535,536,539,540,543],{},[207,530,427],{}," Strength decides what happens to children when a parent is deleted or copied. ",[18,533,534],{},"Association"," — the default — means the children are independent: deleting a parcel leaves its inspections orphaned. ",[18,537,538],{},"Composition"," means the children belong to the parent: deleting the parcel deletes its inspections, and copying the parcel copies them. Composition is right when the child records have no meaning without the parent, which is the usual case for inspections, readings and defects, and wrong when the child is a shared reference table. On 3.28 and earlier the enum is ",[18,541,542],{},"QgsRelation.Composition",".",[14,545,546],{},"Because composition deletes data, it is worth setting deliberately rather than by default, and worth stating in whatever documentation the project carries.",[196,548,550],{"id":549},"reading-related-features-from-python","Reading related features from Python",[14,552,553],{},[38,554,557,560,563,566,573,576,581,586,595,600,605,619,623,628,631,635,639,644,649,653,656,660,663,667],{"viewBox":555,"role":41,"ariaLabel":556,"xmlns":43},"0 0 760 300","Two directions of traversal: from a parent to its children with getRelatedFeatures, and from a child to its parent with getReferencedFeature",[45,558,559],{},"Both directions, two methods",[49,561,562],{},"From a parent feature, getRelatedFeatures returns a request that iterates its children. From a child feature, getReferencedFeature returns the single parent it points at. Both come from the relation object, and neither requires writing the join condition by hand.",[53,564],{"x":55,"y":55,"width":56,"height":565,"fill":58},"300",[60,567,568],{},[63,569,571],{"id":570,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"relTravArrow",[72,572],{"d":74,"fill":75},[77,574,575],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"The relation knows the join; you never write it",[53,577],{"x":117,"y":578,"width":579,"height":580,"rx":91,"fill":92,"stroke":93,"style":94},"46","344","188",[77,582,585],{"x":583,"y":584,"style":99,"fill":93,"textAnchor":83},"196","72","parent → children",[53,587],{"x":588,"y":589,"width":590,"height":591,"rx":592,"fill":106,"stroke":593,"style":594},"60","92","110","34","6","#15803d","stroke-width:1.8",[77,596,599],{"x":597,"y":598,"style":134,"fill":75,"textAnchor":83},"115","114","one parcel",[167,601],{"x1":602,"y1":603,"x2":583,"y2":603,"stroke":75,"style":604},"170","109","stroke-width:1.8;marker-end:url(#relTravArrow)",[606,607,609,615,617],"g",{"fill":106,"stroke":593,"style":608},"stroke-width:1.6",[53,610],{"x":611,"y":612,"width":174,"height":613,"rx":614},"204","90","20","3",[53,616],{"x":611,"y":598,"width":174,"height":613,"rx":614},[53,618],{"x":611,"y":170,"width":174,"height":613,"rx":614},[77,620,622],{"x":621,"y":90,"style":134,"fill":75,"textAnchor":83},"268","many inspections",[77,624,627],{"x":583,"y":625,"style":626,"fill":93,"textAnchor":83},"212","text-anchor:middle;font-size:10.5px;font-family:monospace","getRelatedFeatures(parent)",[53,629],{"x":630,"y":578,"width":579,"height":580,"rx":91,"fill":139,"stroke":140,"style":94},"392",[77,632,634],{"x":633,"y":584,"style":99,"fill":140,"textAnchor":83},"564","child → parent",[53,636],{"x":637,"y":638,"width":174,"height":87,"rx":592,"fill":148,"stroke":140,"style":594},"424","106",[77,640,643],{"x":641,"y":642,"style":134,"fill":75,"textAnchor":83},"488","126","one inspection",[167,645],{"x1":646,"y1":647,"x2":648,"y2":647,"stroke":75,"style":604},"552","121","578",[53,650],{"x":651,"y":638,"width":652,"height":87,"rx":592,"fill":148,"stroke":140,"style":594},"586","120",[77,654,599],{"x":655,"y":642,"style":134,"fill":75,"textAnchor":83},"646",[77,657,659],{"x":633,"y":90,"style":134,"fill":658,"textAnchor":83},"#59645f","exactly one, or an invalid feature",[77,661,662],{"x":633,"y":625,"style":626,"fill":140,"textAnchor":83},"getReferencedFeature(child)",[53,664],{"x":652,"y":665,"width":666,"height":591,"rx":67,"fill":182,"stroke":183,"style":594},"256","520",[77,668,670],{"x":79,"y":669,"style":193,"fill":75,"textAnchor":83},"278","check isValid() on the returned parent — an orphan child returns an invalid one",[222,672,674],{"className":224,"code":673,"language":226,"meta":227,"style":227},"relation = project.relationManager().relation(\"parcel_inspections\")\n\nparent = next(parcels.getFeatures())\nrequest = relation.getRelatedFeaturesRequest(parent)\nfor child in inspections.getFeatures(request):\n    print(child[\"inspected_on\"], child[\"condition\"])\n\nchild = next(inspections.getFeatures())\nowner = relation.getReferencedFeature(child)\nprint(\"belongs to\", owner[\"parcel_id\"] if owner.isValid() else \"nothing\")\n",[18,675,676,689,693,706,716,730,750,754,766,776],{"__ignoreMap":227},[231,677,678,680,682,685,687],{"class":167,"line":233},[231,679,320],{"class":240},[231,681,263],{"class":236},[231,683,684],{"class":240}," project.relationManager().relation(",[231,686,334],{"class":280},[231,688,337],{"class":240},[231,690,691],{"class":167,"line":250},[231,692,254],{"emptyLinePlaceholder":253},[231,694,695,698,700,703],{"class":167,"line":257},[231,696,697],{"class":240},"parent ",[231,699,263],{"class":236},[231,701,702],{"class":287}," next",[231,704,705],{"class":240},"(parcels.getFeatures())\n",[231,707,708,711,713],{"class":167,"line":269},[231,709,710],{"class":240},"request ",[231,712,263],{"class":236},[231,714,715],{"class":240}," relation.getRelatedFeaturesRequest(parent)\n",[231,717,718,721,724,727],{"class":167,"line":293},[231,719,720],{"class":236},"for",[231,722,723],{"class":240}," child ",[231,725,726],{"class":236},"in",[231,728,729],{"class":240}," inspections.getFeatures(request):\n",[231,731,732,735,738,741,744,747],{"class":167,"line":312},[231,733,734],{"class":287},"    print",[231,736,737],{"class":240},"(child[",[231,739,740],{"class":280},"\"inspected_on\"",[231,742,743],{"class":240},"], child[",[231,745,746],{"class":280},"\"condition\"",[231,748,749],{"class":240},"])\n",[231,751,752],{"class":167,"line":317},[231,753,254],{"emptyLinePlaceholder":253},[231,755,756,759,761,763],{"class":167,"line":328},[231,757,758],{"class":240},"child ",[231,760,263],{"class":236},[231,762,702],{"class":287},[231,764,765],{"class":240},"(inspections.getFeatures())\n",[231,767,768,771,773],{"class":167,"line":340},[231,769,770],{"class":240},"owner ",[231,772,263],{"class":236},[231,774,775],{"class":240}," relation.getReferencedFeature(child)\n",[231,777,778,781,783,786,789,791,794,796,799,802,805],{"class":167,"line":351},[231,779,780],{"class":287},"print",[231,782,406],{"class":240},[231,784,785],{"class":280},"\"belongs to\"",[231,787,788],{"class":240},", owner[",[231,790,375],{"class":280},[231,792,793],{"class":240},"] ",[231,795,388],{"class":236},[231,797,798],{"class":240}," owner.isValid() ",[231,800,801],{"class":236},"else",[231,803,804],{"class":280}," \"nothing\"",[231,806,337],{"class":240},[14,808,809,428,811,814,815,818,819,822,823,826,827,829],{},[207,810,427],{},[18,812,813],{},"getRelatedFeaturesRequest"," returns a ",[18,816,817],{},"QgsFeatureRequest"," with the filter already built, which you then pass to the child layer's ",[18,820,821],{},"getFeatures"," — the relation does not fetch for you. That indirection is useful, because you can add a further filter or a subset of attributes to the request before running it. ",[18,824,825],{},"getReferencedFeature"," returns a feature that may be invalid when the child's key matches no parent, and checking ",[18,828,455],{}," is what turns a silent wrong answer into a detected orphan. Finding all orphans is that check in a loop, and it is a good data-quality report to run once on any dataset you did not create.",[196,831,833],{"id":832},"auditing-the-relation-against-the-data","Auditing the relation against the data",[14,835,836],{},"A relation describes what you believe about the data. Checking that belief takes a few lines and is worth doing once on any dataset you inherited.",[222,838,840],{"className":224,"code":839,"language":226,"meta":227,"style":227},"parent_keys = {f[\"parcel_id\"] for f in parcels.getFeatures()}\n\norphans = []\nduplicates = len(parent_keys) != parcels.featureCount()\n\nfor child in inspections.getFeatures():\n    key = child[\"parcel_fk\"]\n    if key is None or key not in parent_keys:\n        orphans.append(child.id())\n\nprint(f\"{len(orphans)} orphaned children\")\nprint(\"parent key is not unique\" if duplicates else \"parent key is unique\")\n",[18,841,842,866,870,880,899,903,914,928,956,961,965,991],{"__ignoreMap":227},[231,843,844,847,849,852,854,856,858,861,863],{"class":167,"line":233},[231,845,846],{"class":240},"parent_keys ",[231,848,263],{"class":236},[231,850,851],{"class":240}," {f[",[231,853,375],{"class":280},[231,855,793],{"class":240},[231,857,720],{"class":236},[231,859,860],{"class":240}," f ",[231,862,726],{"class":236},[231,864,865],{"class":240}," parcels.getFeatures()}\n",[231,867,868],{"class":167,"line":250},[231,869,254],{"emptyLinePlaceholder":253},[231,871,872,875,877],{"class":167,"line":257},[231,873,874],{"class":240},"orphans ",[231,876,263],{"class":236},[231,878,879],{"class":240}," []\n",[231,881,882,885,887,890,893,896],{"class":167,"line":269},[231,883,884],{"class":240},"duplicates ",[231,886,263],{"class":236},[231,888,889],{"class":287}," len",[231,891,892],{"class":240},"(parent_keys) ",[231,894,895],{"class":236},"!=",[231,897,898],{"class":240}," parcels.featureCount()\n",[231,900,901],{"class":167,"line":293},[231,902,254],{"emptyLinePlaceholder":253},[231,904,905,907,909,911],{"class":167,"line":312},[231,906,720],{"class":236},[231,908,723],{"class":240},[231,910,726],{"class":236},[231,912,913],{"class":240}," inspections.getFeatures():\n",[231,915,916,919,921,924,926],{"class":167,"line":317},[231,917,918],{"class":240},"    key ",[231,920,263],{"class":236},[231,922,923],{"class":240}," child[",[231,925,369],{"class":280},[231,927,290],{"class":240},[231,929,930,933,936,939,942,945,947,950,953],{"class":167,"line":328},[231,931,932],{"class":236},"    if",[231,934,935],{"class":240}," key ",[231,937,938],{"class":236},"is",[231,940,941],{"class":287}," None",[231,943,944],{"class":236}," or",[231,946,935],{"class":240},[231,948,949],{"class":236},"not",[231,951,952],{"class":236}," in",[231,954,955],{"class":240}," parent_keys:\n",[231,957,958],{"class":167,"line":340},[231,959,960],{"class":240},"        orphans.append(child.id())\n",[231,962,963],{"class":167,"line":351},[231,964,254],{"emptyLinePlaceholder":253},[231,966,967,969,971,974,977,980,983,986,989],{"class":167,"line":357},[231,968,780],{"class":287},[231,970,406],{"class":240},[231,972,973],{"class":236},"f",[231,975,976],{"class":280},"\"",[231,978,979],{"class":287},"{len",[231,981,982],{"class":240},"(orphans)",[231,984,985],{"class":287},"}",[231,987,988],{"class":280}," orphaned children\"",[231,990,337],{"class":240},[231,992,993,995,997,1000,1003,1006,1008,1011],{"class":167,"line":363},[231,994,780],{"class":287},[231,996,406],{"class":240},[231,998,999],{"class":280},"\"parent key is not unique\"",[231,1001,1002],{"class":236}," if",[231,1004,1005],{"class":240}," duplicates ",[231,1007,801],{"class":236},[231,1009,1010],{"class":280}," \"parent key is unique\"",[231,1012,337],{"class":240},[14,1014,1015,1017],{},[207,1016,427],{}," Building the parent key set once and testing membership makes this linear rather than quadratic, which matters on any real dataset. Comparing the set's size against the feature count is the cheapest possible uniqueness check — if they differ, some key value appears twice, and every child with that key will be attached to both parents, which QGIS will happily display without comment. Treating a null foreign key as an orphan is the usual convention; where nulls legitimately mean \"not yet assigned\", count them separately so the number of genuine breakages stays visible.",[14,1019,1020],{},"Running this before and after a data load is a cheap regression test, and it catches the class of problem — a key column re-typed from text to integer somewhere in an export chain — that makes a working relation stop matching without anything visibly changing.",[196,1022,1024],{"id":1023},"making-the-form-useful","Making the form useful",[14,1026,1027],{},"A relation on its own gives the parent's attribute form a child table. Two settings make it pleasant rather than merely present:",[222,1029,1031],{"className":224,"code":1030,"language":226,"meta":227,"style":227},"from qgis.core import QgsEditorWidgetSetup\n\nconfig = {\"Relation\": \"parcel_inspections\", \"ShowForm\": False, \"AllowAddFeatures\": True}\nsetup = QgsEditorWidgetSetup(\"RelationReference\", config)\n",[18,1032,1033,1044,1048,1089],{"__ignoreMap":227},[231,1034,1035,1037,1039,1041],{"class":167,"line":233},[231,1036,237],{"class":236},[231,1038,241],{"class":240},[231,1040,244],{"class":236},[231,1042,1043],{"class":240}," QgsEditorWidgetSetup\n",[231,1045,1046],{"class":167,"line":250},[231,1047,254],{"emptyLinePlaceholder":253},[231,1049,1050,1053,1055,1058,1061,1064,1066,1068,1071,1073,1076,1078,1081,1083,1086],{"class":167,"line":257},[231,1051,1052],{"class":240},"config ",[231,1054,263],{"class":236},[231,1056,1057],{"class":240}," {",[231,1059,1060],{"class":280},"\"Relation\"",[231,1062,1063],{"class":240},": ",[231,1065,334],{"class":280},[231,1067,372],{"class":240},[231,1069,1070],{"class":280},"\"ShowForm\"",[231,1072,1063],{"class":240},[231,1074,1075],{"class":287},"False",[231,1077,372],{"class":240},[231,1079,1080],{"class":280},"\"AllowAddFeatures\"",[231,1082,1063],{"class":240},[231,1084,1085],{"class":287},"True",[231,1087,1088],{"class":240},"}\n",[231,1090,1091,1094,1096,1099,1102],{"class":167,"line":269},[231,1092,1093],{"class":240},"setup ",[231,1095,263],{"class":236},[231,1097,1098],{"class":240}," QgsEditorWidgetSetup(",[231,1100,1101],{"class":280},"\"RelationReference\"",[231,1103,1104],{"class":240},", config)\n",[14,1106,1107,1109,1110,1113,1114,1117,1118,1121],{},[207,1108,427],{}," The relation-reference widget on the ",[434,1111,1112],{},"child's"," foreign key field turns a free-text box into a picker of valid parents, which is the single biggest data-quality improvement a relation enables — a typed key can be wrong, a picked one cannot. ",[18,1115,1116],{},"ShowForm"," false keeps the embedded parent form collapsed so the child form stays compact. The parent side's child table is configured through the form layout rather than a widget, and it is where ",[18,1119,1120],{},"AllowAddFeatures"," decides whether users can create children in place.",[196,1123,1125],{"id":1124},"discovering-the-relations-a-project-already-has","Discovering the relations a project already has",[14,1127,1128],{},"Before adding one, it is worth seeing what is there — a project inherited from someone else frequently has relations you did not expect, and duplicated relations with different ids are a common cause of a form showing the same child table twice.",[222,1130,1132],{"className":224,"code":1131,"language":226,"meta":227,"style":227},"manager = project.relationManager()\n\nfor relation_id, existing in manager.relations().items():\n    parent_layer = existing.referencedLayer()\n    child_layer = existing.referencingLayer()\n    pairs = existing.fieldPairs()\n    print(f\"{relation_id}: {child_layer.name()} → {parent_layer.name()} {pairs} \"\n          f\"strength={existing.strength()}\")\n",[18,1133,1134,1144,1148,1160,1170,1180,1190,1237],{"__ignoreMap":227},[231,1135,1136,1139,1141],{"class":167,"line":233},[231,1137,1138],{"class":240},"manager ",[231,1140,263],{"class":236},[231,1142,1143],{"class":240}," project.relationManager()\n",[231,1145,1146],{"class":167,"line":250},[231,1147,254],{"emptyLinePlaceholder":253},[231,1149,1150,1152,1155,1157],{"class":167,"line":257},[231,1151,720],{"class":236},[231,1153,1154],{"class":240}," relation_id, existing ",[231,1156,726],{"class":236},[231,1158,1159],{"class":240}," manager.relations().items():\n",[231,1161,1162,1165,1167],{"class":167,"line":269},[231,1163,1164],{"class":240},"    parent_layer ",[231,1166,263],{"class":236},[231,1168,1169],{"class":240}," existing.referencedLayer()\n",[231,1171,1172,1175,1177],{"class":167,"line":293},[231,1173,1174],{"class":240},"    child_layer ",[231,1176,263],{"class":236},[231,1178,1179],{"class":240}," existing.referencingLayer()\n",[231,1181,1182,1185,1187],{"class":167,"line":312},[231,1183,1184],{"class":240},"    pairs ",[231,1186,263],{"class":236},[231,1188,1189],{"class":240}," existing.fieldPairs()\n",[231,1191,1192,1194,1196,1198,1200,1203,1206,1208,1210,1212,1215,1217,1220,1222,1225,1227,1229,1232,1234],{"class":167,"line":317},[231,1193,734],{"class":287},[231,1195,406],{"class":240},[231,1197,973],{"class":236},[231,1199,976],{"class":280},[231,1201,1202],{"class":287},"{",[231,1204,1205],{"class":240},"relation_id",[231,1207,985],{"class":287},[231,1209,1063],{"class":280},[231,1211,1202],{"class":287},[231,1213,1214],{"class":240},"child_layer.name()",[231,1216,985],{"class":287},[231,1218,1219],{"class":280}," → ",[231,1221,1202],{"class":287},[231,1223,1224],{"class":240},"parent_layer.name()",[231,1226,985],{"class":287},[231,1228,1057],{"class":287},[231,1230,1231],{"class":240},"pairs",[231,1233,985],{"class":287},[231,1235,1236],{"class":280}," \"\n",[231,1238,1239,1242,1245,1247,1250,1252,1254],{"class":167,"line":328},[231,1240,1241],{"class":236},"          f",[231,1243,1244],{"class":280},"\"strength=",[231,1246,1202],{"class":287},[231,1248,1249],{"class":240},"existing.strength()",[231,1251,985],{"class":287},[231,1253,976],{"class":280},[231,1255,337],{"class":240},[14,1257,1258,428,1260,1263,1264,1267,1268,1270,1271,1274],{},[207,1259,427],{},[18,1261,1262],{},"relations()"," returns a dictionary keyed by relation id, and ",[18,1265,1266],{},"fieldPairs()"," gives the referencing-to-referenced mapping as a dictionary, so this one loop tells you the whole relational structure of a project. Printing the strength alongside is worth it because a ",[18,1269,538],{}," relation nobody knew about is a data-loss risk the moment somebody deletes a parent. Removing one is ",[18,1272,1273],{},"manager.removeRelation(relation_id)",", and it takes effect immediately in the forms.",[14,1276,1277,1278,1282],{},"For a project that will be handed on, generating this listing into the project's own metadata — as described in ",[26,1279,1281],{"href":1280},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fread-and-write-layer-metadata-pyqgis\u002F","reading and writing layer metadata"," — means the next person does not have to run a script to find out how the data fits together.",[196,1284,1286],{"id":1285},"qgis-version-compatibility","QGIS version compatibility",[14,1288,1289,1291,1292,1295,1296,1299],{},[18,1290,33],{}," has been present since QGIS 2.x with a stable API. Relation strength arrived in 3.0 as ",[18,1293,1294],{},"QgsRelation.RelationStrength"," and moved to ",[18,1297,1298],{},"Qgis.RelationshipStrength"," in 3.28, with the old name retained. Many-to-many relations through a linking table are configured as two one-to-many relations, which has been the approach throughout. In 3.28 a broader relationship API for provider-declared relationships was added; project relations as described here are unchanged.",[196,1301,1303],{"id":1302},"troubleshooting","Troubleshooting",[201,1305,1306,1314,1320,1326,1332,1340],{},[204,1307,1308,1313],{},[207,1309,1310,1312],{},[18,1311,455],{}," is False."," A layer id is wrong, or a field name does not exist on the layer you named.",[204,1315,1316,1319],{},[207,1317,1318],{},"The relation is valid but finds no children."," The field pair is reversed.",[204,1321,1322,1325],{},[207,1323,1324],{},"Children appear under the wrong parent."," The parent key is not unique.",[204,1327,1328,1331],{},[207,1329,1330],{},"The relation disappears after reopening the project."," It was never added to the relation manager, only constructed.",[204,1333,1334,1339],{},[207,1335,1336,1338],{},[18,1337,20],{}," returns NULL."," It takes the relation id, not the name.",[204,1341,1342,1345,1346,1348,1349,1351],{},[207,1343,1344],{},"Deleting a parent leaves orphans."," Strength is ",[18,1347,534],{},"; set ",[18,1350,538],{}," if that is the intent.",[196,1353,1355],{"id":1354},"conclusion","Conclusion",[14,1357,1358],{},"Declare the relation with the child field first, give it a stable id, choose the strength deliberately, and register it with the relation manager so it lives in the project. Then use the relation object to traverse rather than writing join filters by hand — it knows the mapping, and it keeps working when the mapping changes.",[196,1360,1362],{"id":1361},"frequently-asked-questions","Frequently Asked Questions",[14,1364,1365,1368],{},[207,1366,1367],{},"Can a relation span two different data sources?","\nYes. The layers can be a GeoPackage and a PostGIS table, or anything else QGIS can load, because the relation is a project-level concept rather than a database constraint.",[14,1370,1371,1374],{},[207,1372,1373],{},"How do I express many-to-many?","\nAs two one-to-many relations against a linking table. QGIS's form widgets understand that pattern and present it as a single list.",[14,1376,1377,1380],{},[207,1378,1379],{},"Does a relation enforce referential integrity?","\nNo. It describes a relationship for QGIS's benefit; nothing stops a child pointing at a non-existent parent. Database-level constraints are the place for enforcement.",[14,1382,1383,1386,1387,543],{},[207,1384,1385],{},"Are relations included when I save a project as a template?","\nYes, they are part of the project file, which is why registering them matters — see ",[26,1388,1390],{"href":1389},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fsave-and-load-qgis-project-pyqgis\u002F","saving and loading a QGIS project",[196,1392,1394],{"id":1393},"related","Related",[201,1396,1397,1403,1409,1415,1420],{},[204,1398,1399,1402],{},[26,1400,1401],{"href":28},"Working with QGIS Projects in PyQGIS"," — the guide this recipe belongs to",[204,1404,1405],{},[26,1406,1408],{"href":1407},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fuse-aggregate-expressions-pyqgis\u002F","Use Aggregate Expressions in PyQGIS",[204,1410,1411],{},[26,1412,1414],{"href":1413},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fjoin-attributes-by-field-value-pyqgis\u002F","Join Attributes by Field Value in PyQGIS",[204,1416,1417],{},[26,1418,1419],{"href":1389},"Save and Load a QGIS Project in PyQGIS",[204,1421,1422],{},[26,1423,1424],{"href":1280},"Read and Write Layer Metadata in PyQGIS",[1426,1427,1428],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":227,"searchDepth":250,"depth":250,"links":1430},[1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442],{"id":198,"depth":250,"text":199},{"id":219,"depth":250,"text":220},{"id":500,"depth":250,"text":501},{"id":549,"depth":250,"text":550},{"id":832,"depth":250,"text":833},{"id":1023,"depth":250,"text":1024},{"id":1124,"depth":250,"text":1125},{"id":1285,"depth":250,"text":1286},{"id":1302,"depth":250,"text":1303},{"id":1354,"depth":250,"text":1355},{"id":1361,"depth":250,"text":1362},{"id":1393,"depth":250,"text":1394},"Wire parent and child layers together with QgsRelation — one-to-many relations, referencing and referenced fields, relation strength, and reading related features from Python.","md",{"slug":1446,"type":1447,"breadcrumb":1448,"datePublished":1449,"dateModified":1449},"define-layer-relations-pyqgis","article","Define Layer Relations","2026-09-05","\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fdefine-layer-relations-pyqgis",{"title":5,"description":1443},"pyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fdefine-layer-relations-pyqgis\u002Findex","bZIaTpicYBxoAJWmzfESEr8zvRv3nZhc08UuTXni3J4",1788563847753]